diff --git a/doc/xml/release.xml b/doc/xml/release.xml index ed7bf0ae9..03b18ebc0 100644 --- a/doc/xml/release.xml +++ b/doc/xml/release.xml @@ -35,6 +35,18 @@

Block incremental backup.

+ + + + + + + + + +

SFTP support for repository storage.

+
+ diff --git a/doc/xml/user-guide.xml b/doc/xml/user-guide.xml index 97e1d17e5..d8ef99ce1 100644 --- a/doc/xml/user-guide.xml +++ b/doc/xml/user-guide.xml @@ -169,8 +169,15 @@ accessKey1 verySecretKey1 + + n + demo-repo + sha1 + {[pg-home-path]}/.ssh/id_rsa_sftp + {[pg-home-path]}/.ssh/id_rsa_sftp.pub + - ('{[azure-all]}' eq 'y' || '{[gcs-all]}' eq 'y' || '{[s3-all]}' eq 'y') + ('{[azure-all]}' eq 'y' || '{[gcs-all]}' eq 'y' || '{[s3-all]}' eq 'y' || '{[sftp-all]}' eq 'y') pgbackrest/doc:{[os-type]} @@ -192,6 +199,12 @@ s3 s3-server + sftp + sftp-server + {[host-user]} + {[host-image]} + {[host-mount]} + pg1 pg-primary {[host-user]} @@ -300,7 +313,7 @@ rm /etc/apt/apt.conf.d/70debconf && \ apt-get update && \ apt-get install -y --no-install-recommends sudo ssh wget vim gnupg lsb-release iputils-ping ca-certificates \ - tzdata locales 2>&1 + tzdata locales libssh2-1-dev 2>&1 {[sudo-disable-core-dump]} @@ -361,6 +374,10 @@ # Enable PowerTools repository (only available on RHEL8) RUN dnf config-manager --set-enabled powertools || true + # Install and enable epel repository + RUN dnf -y install epel-release + RUN crb enable + # Install CA certificate RUN update-ca-trust extract @@ -551,13 +568,13 @@ - apt-get install postgresql-client libxml2 + apt-get install postgresql-client libxml2 libssh2-1 -y 2>&1 - yum install postgresql-libs + yum install postgresql-libs libssh2 -y 2>&1 @@ -773,6 +790,57 @@ The region and endpoint will need to be configured to where the bucket is located. The values given here are for the {[s3-region]} region. + + +

supports locating repositories on SFTP hosts. SFTP file transfer is relatively slow so commands benefit by increasing process-max to parallelize file transfer.

+ + + Configure <proper>SFTP</proper> + + sftp + /{[sftp-repo]} + y + {[host-sftp]} + {[sftp-host-key-hash-type]} + {[sftp-setup-user]} + {[sftp-private-key-file]} + {[sftp-public-key-file]} + 4 + + +

When utilizing SFTP, if libssh2 is compiled against OpenSSH then repo{[sftp-setup-repo-id]}-sftp-public-key-file is optional.

+ + + Generate ssh keypair for sftp backup + + + ssh-keygen -f {[pg-home-path]}/.ssh/id_rsa_sftp + -t rsa -b 4096 -N "" -m PEM + + + + + Copy <host>{[host-pg1]}</host> sftp backup public key to <host>{[host-sftp]}</host> + + + mkdir -m 750 -p {[pg-home-path]}/.ssh + + + + + mkdir -m 750 -p /{[sftp-repo]} && chown {[sftp-setup-user]}:{[pg-group]} /{[sftp-repo]} + + + + + + (sudo ssh root@{[host-pg1]} cat {[pg-home-path]}/.ssh/id_rsa_sftp.pub) | + sudo -u {[sftp-setup-user]} tee -a {[pg-home-path]}/.ssh/authorized_keys + + + +
+
Introduction @@ -783,6 +851,9 @@ + + +

This user guide is intended to be followed sequentially from beginning to end &mdash; each section depends on the last. For example, the Restore section relies on setup that is performed in the Quick Start section. Once is up and running then skipping around is possible but following the user guide in order is recommended the first time through.

Although the examples in this guide are targeted at {[user-guide-os]} and {[pg-version]}, it should be fairly easy to apply the examples to any Unix distribution and version. The only OS-specific commands are those to create, start, stop, and drop clusters. The commands will be the same on any Unix system though the location of the executable may vary. While strives to operate consistently across versions of , there are subtle differences between versions of that may show up in this guide when illustrating certain examples, e.g. path/file names and settings.

@@ -925,15 +996,15 @@ apt-get install make gcc libpq-dev libssl-dev libxml2-dev pkg-config - liblz4-dev libzstd-dev libbz2-dev libz-dev libyaml-dev + liblz4-dev libzstd-dev libbz2-dev libz-dev libyaml-dev libssh2-1-dev -y 2>&1 - yum install make gcc postgresql{[pg-version-nodot]}-devel - openssl-devel libxml2-devel lz4-devel libzstd-devel bzip2-devel libyaml-devel + yum install make gcc postgresql{[pg-version-nodot]}-devel openssl-devel + libxml2-devel lz4-devel libzstd-devel bzip2-devel libyaml-devel libssh2-devel -y 2>&1 @@ -1147,6 +1218,19 @@
+ +
+ SFTP Storage Support + + + 1 + {[host-pg1]} + postgres + postgres:postgres + {[pg-home-path]} + +
+
Configure Archiving @@ -2401,12 +2485,45 @@
+ +
+ SFTP Support + + + 4 + {[host-pg1]} + postgres + postgres:postgres + + +

Commands are run exactly as if the repository were stored on a local disk.

+ + + Create the stanza + + + {[project-exe]} {[dash]}-stanza={[postgres-cluster-demo]} {[dash]}-log-level-console=info stanza-create + completed successfully + + + + + Backup the {[postgres-cluster-demo]} cluster + + + {[project-exe]} {[dash]}-stanza={[postgres-cluster-demo]} --repo=4 + --log-level-console=info backup + no prior backup exists|full backup size + + +
+
GCS-Compatible Object Store Support - 4 + 5 {[host-pg1]} postgres postgres:postgres @@ -2655,6 +2772,15 @@ {[br-user]}:{[br-group]} n + +

Configure SFTP storage if required.

+ + + 1 + {[host-repo1]} + {[br-user]} + {[br-user]}:{[br-group]} +
diff --git a/meson.build b/meson.build index fdac5d541..ea36c9870 100644 --- a/meson.build +++ b/meson.build @@ -165,6 +165,13 @@ lib_yaml = dependency('yaml-0.1') # Find required gz library lib_z = dependency('zlib') +# Find optional libssh2 library +lib_ssh2 = dependency('libssh2', required: false) + +if lib_ssh2.found() + configuration.set('HAVE_LIBSSH2', true, description: 'Is libssh2 present?') +endif + # Find optional zstd library lib_zstd = dependency('libzstd', version: '>=1.0', required: false) diff --git a/src/Makefile.in b/src/Makefile.in index 17bced88d..65ae9f6f8 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -187,7 +187,11 @@ SRCS = \ storage/s3/helper.c \ storage/s3/read.c \ storage/s3/storage.c \ - storage/s3/write.c + storage/s3/write.c \ + storage/sftp/helper.c \ + storage/sftp/read.c \ + storage/sftp/storage.c \ + storage/sftp/write.c #################################################################################################################################### # Compiler options diff --git a/src/build.auto.h.in b/src/build.auto.h.in index 14bc69d64..85d3fe110 100644 --- a/src/build.auto.h.in +++ b/src/build.auto.h.in @@ -23,6 +23,9 @@ Build Flags Generated by Configure // Is libzstd present? #undef HAVE_LIBZST +// Is libssh2 present? +#undef HAVE_LIBSSH2 + // Configuration path #undef CFGOPTDEF_CONFIG_PATH diff --git a/src/build/config/config.yaml b/src/build/config/config.yaml index bf19899dd..d1101f1d6 100644 --- a/src/build/config/config.yaml +++ b/src/build/config/config.yaml @@ -1735,6 +1735,7 @@ option: - gcs - posix - s3 + - sftp command: annotate: command-role: @@ -2350,6 +2351,75 @@ option: command: repo-type depend: repo-s3-bucket + repo-sftp-host: + section: global + group: repo + type: string + command: repo-type + depend: + option: repo-type + list: + - sftp + + repo-sftp-host-fingerprint: + section: global + group: repo + type: string + required: false + command: repo-type + depend: repo-sftp-host + + repo-sftp-host-key-hash-type: + section: global + group: repo + type: string-id + allow-list: + - md5 + - sha1 + - sha256 + command: repo-type + depend: repo-sftp-host + + repo-sftp-host-port: + section: global + group: repo + type: integer + default: 22 + allow-range: [1, 65535] + command: repo-type + depend: repo-sftp-host + + repo-sftp-host-user: + section: global + group: repo + type: string + command: repo-type + depend: repo-sftp-host + + repo-sftp-private-key-file: + section: global + group: repo + type: string + command: repo-type + depend: repo-sftp-host + + repo-sftp-private-key-passphrase: + section: global + group: repo + type: string + required: false + secure: true + command: repo-type + depend: repo-sftp-host + + repo-sftp-public-key-file: + section: global + group: repo + type: string + required: false + command: repo-type + depend: repo-sftp-host + repo-storage-verify-tls: section: global group: repo diff --git a/src/build/configure.ac b/src/build/configure.ac index aff9f4289..212f659fa 100644 --- a/src/build/configure.ac +++ b/src/build/configure.ac @@ -106,6 +106,17 @@ AC_CHECK_LIB( [AC_CHECK_HEADER(lz4frame.h, [AC_DEFINE(HAVE_LIBLZ4) AC_SUBST(LIBS, "${LIBS} -llz4")], [AC_MSG_ERROR([header file is required])])]) +# Check optional libSSH2 library +# ---------------------------------------------------------------------------------------------------------------------------------- +AC_CHECK_LIB( + [ssh2], [libssh2_init], + [AC_CHECK_HEADER(libssh2.h, [AC_DEFINE(HAVE_LIBSSH2) AC_SUBST(LIBS, "${LIBS} -lssh2")], + [AC_MSG_ERROR([header file is required])])]) +AC_CHECK_LIB( + [ssh2], [libssh2_sftp_init], + [AC_CHECK_HEADER(libssh2_sftp.h, [], + [AC_MSG_ERROR([header file is required])])]) + # Check optional zst library. Ignore any versions below 1.0. # ---------------------------------------------------------------------------------------------------------------------------------- AC_CHECK_LIB( diff --git a/src/build/help/help.xml b/src/build/help/help.xml index 28f6d9b77..dddee3691 100644 --- a/src/build/help/help.xml +++ b/src/build/help/help.xml @@ -1005,6 +1005,86 @@ path + + SFTP repository host. + + +

The SFTP host containing the repository.

+
+ + sftprepo.domain +
+ + + SFTP repository host fingerprint. + + +

SFTP repository host fingerprint generation should match the repo-sftp-host-key-hash-type. Generate the fingeprint via awk '{print $2}' ssh_host_xxx_key.pub | base64 -d | (md5sum or sha1sum) -b. The ssh host keys are normally found in the /etc/ssh directory.

+
+ + f84e172dfead7aeeeae6c1fdfb5aa8cf +
+ + + SFTP repository host key hash type. + + +

SFTP repository host key hash type. Declares the hash type to be used to compute the digest of the remote system's host key on SSH startup. Newer versions of libssh2 support sha256 in addition to md5 and sha1.

+
+ + sha256 +
+ + + SFTP repository host port. + + +

SFTP repository host port.

+
+ + 22 +
+ + + SFTP repository host user. + + +

User on the host used to store the repository.

+
+ + pg-backup +
+ + + SFTP private key file. + + +

SFTP private key file used for authentication.

+
+ + ~/.ssh/id_ed25519 +
+ + + SFTP private key passphrase. + + +

Passphrase used to access the private key. This is an optional feature when creating an SSH public/private key pair.

+
+ + BeSureToGenerateAndUseASecurePassphrase +
+ + + SFTP public key file. + + +

SFTP public key file used for authentication. Optional if compiled against OpenSSL, required if compiled against a different library.

+
+ + ~/.ssh/id_ed25519.pub +
+ Repository storage CA file. @@ -1089,6 +1169,7 @@ gcs - Google Cloud Storage posix - Posix-compliant file systems s3 - AWS Simple Storage Service + sftp - Secure File Transfer Protocol

When an NFS mount is used as a posix repository, the same rules apply to as described in the documentation: Creating a Database Cluster - File Systems.

diff --git a/src/config/config.auto.h b/src/config/config.auto.h index d112b5038..cfa2552df 100644 --- a/src/config/config.auto.h +++ b/src/config/config.auto.h @@ -135,7 +135,7 @@ Option constants #define CFGOPT_TYPE "type" #define CFGOPT_VERBOSE "verbose" -#define CFG_OPTION_TOTAL 167 +#define CFG_OPTION_TOTAL 175 /*********************************************************************************************************************************** Option value constants @@ -269,6 +269,13 @@ Option value constants #define CFGOPTVAL_REPO_S3_URI_STYLE_PATH STRID5("path", 0x450300) #define CFGOPTVAL_REPO_S3_URI_STYLE_PATH_Z "path" +#define CFGOPTVAL_REPO_SFTP_HOST_KEY_HASH_TYPE_MD5 STRID5("md5", 0x748d0) +#define CFGOPTVAL_REPO_SFTP_HOST_KEY_HASH_TYPE_MD5_Z "md5" +#define CFGOPTVAL_REPO_SFTP_HOST_KEY_HASH_TYPE_SHA1 STRID6("sha1", 0x7412131) +#define CFGOPTVAL_REPO_SFTP_HOST_KEY_HASH_TYPE_SHA1_Z "sha1" +#define CFGOPTVAL_REPO_SFTP_HOST_KEY_HASH_TYPE_SHA256 STRID5("sha256", 0x3dde05130) +#define CFGOPTVAL_REPO_SFTP_HOST_KEY_HASH_TYPE_SHA256_Z "sha256" + #define CFGOPTVAL_REPO_TYPE_AZURE STRID5("azure", 0x5957410) #define CFGOPTVAL_REPO_TYPE_AZURE_Z "azure" #define CFGOPTVAL_REPO_TYPE_CIFS STRID5("cifs", 0x999230) @@ -279,6 +286,8 @@ Option value constants #define CFGOPTVAL_REPO_TYPE_POSIX_Z "posix" #define CFGOPTVAL_REPO_TYPE_S3 STRID6("s3", 0x7d31) #define CFGOPTVAL_REPO_TYPE_S3_Z "s3" +#define CFGOPTVAL_REPO_TYPE_SFTP STRID5("sftp", 0x850d30) +#define CFGOPTVAL_REPO_TYPE_SFTP_Z "sftp" #define CFGOPTVAL_SORT_ASC STRID5("asc", 0xe610) #define CFGOPTVAL_SORT_ASC_Z "asc" @@ -499,6 +508,14 @@ typedef enum cfgOptRepoS3Role, cfgOptRepoS3Token, cfgOptRepoS3UriStyle, + cfgOptRepoSftpHost, + cfgOptRepoSftpHostFingerprint, + cfgOptRepoSftpHostKeyHashType, + cfgOptRepoSftpHostPort, + cfgOptRepoSftpHostUser, + cfgOptRepoSftpPrivateKeyFile, + cfgOptRepoSftpPrivateKeyPassphrase, + cfgOptRepoSftpPublicKeyFile, cfgOptRepoStorageCaFile, cfgOptRepoStorageCaPath, cfgOptRepoStorageHost, diff --git a/src/config/load.c b/src/config/load.c index 215d0e04c..3515c04a7 100644 --- a/src/config/load.c +++ b/src/config/load.c @@ -23,6 +23,7 @@ Configuration Load #include "storage/cifs/storage.h" #include "storage/helper.h" #include "storage/posix/storage.h" +#include "storage/sftp/storage.h" /*********************************************************************************************************************************** Load log settings @@ -86,9 +87,10 @@ cfgLoadUpdateOption(void) { for (unsigned int optionIdx = 0; optionIdx < cfgOptionGroupIdxTotal(cfgOptGrpRepo); optionIdx++) { - // If the repo is local and either posix or cifs + // If the repo is local and either posix, cifs or sftp if (!cfgOptionIdxTest(cfgOptRepoHost, optionIdx) && (cfgOptionIdxStrId(cfgOptRepoType, optionIdx) == STORAGE_POSIX_TYPE || + cfgOptionIdxStrId(cfgOptRepoType, optionIdx) == STORAGE_SFTP_TYPE || cfgOptionIdxStrId(cfgOptRepoType, optionIdx) == STORAGE_CIFS_TYPE)) { // Ensure a local repo does not have the same path as another local repo of the same type diff --git a/src/config/parse.auto.c.inc b/src/config/parse.auto.c.inc index a23d63949..d27c46ce1 100644 --- a/src/config/parse.auto.c.inc +++ b/src/config/parse.auto.c.inc @@ -24,6 +24,7 @@ static const StringPub parseRuleValueStr[] = PARSE_RULE_STRPUB("1MiB"), // val/str PARSE_RULE_STRPUB("2"), // val/str PARSE_RULE_STRPUB("20MiB"), // val/str + PARSE_RULE_STRPUB("22"), // val/str PARSE_RULE_STRPUB("256KiB"), // val/str PARSE_RULE_STRPUB("2MiB"), // val/str PARSE_RULE_STRPUB("3"), // val/str @@ -74,6 +75,7 @@ typedef enum parseRuleValStrQT_1MiB_QT, // val/str/enum parseRuleValStrQT_2_QT, // val/str/enum parseRuleValStrQT_20MiB_QT, // val/str/enum + parseRuleValStrQT_22_QT, // val/str/enum parseRuleValStrQT_256KiB_QT, // val/str/enum parseRuleValStrQT_2MiB_QT, // val/str/enum parseRuleValStrQT_3_QT, // val/str/enum @@ -139,6 +141,7 @@ static const StringId parseRuleValueStrId[] = STRID5("json", 0x73e6a0), // val/strid STRID5("lsn", 0x3a6c0), // val/strid STRID6("lz4", 0x2068c1), // val/strid + STRID5("md5", 0x748d0), // val/strid STRID5("name", 0x2b42e0), // val/strid STRID5("none", 0x2b9ee0), // val/strid STRID5("off", 0x18cf0), // val/strid @@ -152,6 +155,9 @@ static const StringId parseRuleValueStrId[] = STRID6("s3", 0x7d31), // val/strid STRID5("sas", 0x4c330), // val/strid STRID5("service", 0x1469b48b30), // val/strid + STRID5("sftp", 0x850d30), // val/strid + STRID6("sha1", 0x7412131), // val/strid + STRID5("sha256", 0x3dde05130), // val/strid STRID5("shared", 0x85905130), // val/strid STRID5("shutdown", 0x75de4a55130), // val/strid STRID5("ssh", 0x22730), // val/strid @@ -192,6 +198,7 @@ typedef enum parseRuleValStrIdJson, // val/strid/enum parseRuleValStrIdLsn, // val/strid/enum parseRuleValStrIdLz4, // val/strid/enum + parseRuleValStrIdMd5, // val/strid/enum parseRuleValStrIdName, // val/strid/enum parseRuleValStrIdNone, // val/strid/enum parseRuleValStrIdOff, // val/strid/enum @@ -205,6 +212,9 @@ typedef enum parseRuleValStrIdS3, // val/strid/enum parseRuleValStrIdSas, // val/strid/enum parseRuleValStrIdService, // val/strid/enum + parseRuleValStrIdSftp, // val/strid/enum + parseRuleValStrIdSha1, // val/strid/enum + parseRuleValStrIdSha256, // val/strid/enum parseRuleValStrIdShared, // val/strid/enum parseRuleValStrIdShutdown, // val/strid/enum parseRuleValStrIdSsh, // val/strid/enum @@ -232,6 +242,7 @@ static const int64_t parseRuleValueInt[] = 2, // val/int 3, // val/int 9, // val/int + 22, // val/int 32, // val/int 100, // val/int 256, // val/int @@ -281,6 +292,7 @@ typedef enum parseRuleValInt2, // val/int/enum parseRuleValInt3, // val/int/enum parseRuleValInt9, // val/int/enum + parseRuleValInt22, // val/int/enum parseRuleValInt32, // val/int/enum parseRuleValInt100, // val/int/enum parseRuleValInt256, // val/int/enum @@ -7857,6 +7869,666 @@ static const ParseRuleOption parseRuleOption[CFG_OPTION_TOTAL] = ), // opt/repo-s3-uri-style ), // opt/repo-s3-uri-style // ----------------------------------------------------------------------------------------------------------------------------- + PARSE_RULE_OPTION // opt/repo-sftp-host + ( // opt/repo-sftp-host + PARSE_RULE_OPTION_NAME("repo-sftp-host"), // opt/repo-sftp-host + PARSE_RULE_OPTION_TYPE(cfgOptTypeString), // opt/repo-sftp-host + PARSE_RULE_OPTION_RESET(true), // opt/repo-sftp-host + PARSE_RULE_OPTION_REQUIRED(true), // opt/repo-sftp-host + PARSE_RULE_OPTION_SECTION(cfgSectionGlobal), // opt/repo-sftp-host + PARSE_RULE_OPTION_GROUP_MEMBER(true), // opt/repo-sftp-host + PARSE_RULE_OPTION_GROUP_ID(cfgOptGrpRepo), // opt/repo-sftp-host + // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND_ROLE_MAIN_VALID_LIST // opt/repo-sftp-host + ( // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdAnnotate) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdBackup) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdCheck) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdExpire) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdInfo) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdManifest) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoCreate) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoGet) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoLs) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoPut) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoRm) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaCreate) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaDelete) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaUpgrade) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-host + ), // opt/repo-sftp-host + // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND_ROLE_ASYNC_VALID_LIST // opt/repo-sftp-host + ( // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-host + ), // opt/repo-sftp-host + // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND_ROLE_LOCAL_VALID_LIST // opt/repo-sftp-host + ( // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdBackup) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-host + ), // opt/repo-sftp-host + // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND_ROLE_REMOTE_VALID_LIST // opt/repo-sftp-host + ( // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdAnnotate) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdCheck) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdInfo) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdManifest) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoCreate) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoGet) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoLs) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoPut) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoRm) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaCreate) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaDelete) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaUpgrade) // opt/repo-sftp-host + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-host + ), // opt/repo-sftp-host + // opt/repo-sftp-host + PARSE_RULE_OPTIONAL // opt/repo-sftp-host + ( // opt/repo-sftp-host + PARSE_RULE_OPTIONAL_GROUP // opt/repo-sftp-host + ( // opt/repo-sftp-host + PARSE_RULE_OPTIONAL_DEPEND // opt/repo-sftp-host + ( // opt/repo-sftp-host + PARSE_RULE_VAL_OPT(cfgOptRepoType), // opt/repo-sftp-host + PARSE_RULE_VAL_STRID(parseRuleValStrIdSftp), // opt/repo-sftp-host + ), // opt/repo-sftp-host + ), // opt/repo-sftp-host + ), // opt/repo-sftp-host + ), // opt/repo-sftp-host + // ----------------------------------------------------------------------------------------------------------------------------- + PARSE_RULE_OPTION // opt/repo-sftp-host-fingerprint + ( // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_NAME("repo-sftp-host-fingerprint"), // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_TYPE(cfgOptTypeString), // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_RESET(true), // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_REQUIRED(false), // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_SECTION(cfgSectionGlobal), // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_GROUP_MEMBER(true), // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_GROUP_ID(cfgOptGrpRepo), // opt/repo-sftp-host-fingerprint + // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND_ROLE_MAIN_VALID_LIST // opt/repo-sftp-host-fingerprint + ( // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdAnnotate) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdBackup) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdCheck) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdExpire) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdInfo) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdManifest) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoCreate) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoGet) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoLs) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoPut) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoRm) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaCreate) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaDelete) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaUpgrade) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-host-fingerprint + ), // opt/repo-sftp-host-fingerprint + // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND_ROLE_ASYNC_VALID_LIST // opt/repo-sftp-host-fingerprint + ( // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-host-fingerprint + ), // opt/repo-sftp-host-fingerprint + // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND_ROLE_LOCAL_VALID_LIST // opt/repo-sftp-host-fingerprint + ( // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdBackup) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-host-fingerprint + ), // opt/repo-sftp-host-fingerprint + // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND_ROLE_REMOTE_VALID_LIST // opt/repo-sftp-host-fingerprint + ( // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdAnnotate) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdCheck) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdInfo) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdManifest) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoCreate) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoGet) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoLs) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoPut) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoRm) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaCreate) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaDelete) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaUpgrade) // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-host-fingerprint + ), // opt/repo-sftp-host-fingerprint + // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTIONAL // opt/repo-sftp-host-fingerprint + ( // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTIONAL_GROUP // opt/repo-sftp-host-fingerprint + ( // opt/repo-sftp-host-fingerprint + PARSE_RULE_OPTIONAL_DEPEND // opt/repo-sftp-host-fingerprint + ( // opt/repo-sftp-host-fingerprint + PARSE_RULE_VAL_OPT(cfgOptRepoType), // opt/repo-sftp-host-fingerprint + PARSE_RULE_VAL_STRID(parseRuleValStrIdSftp), // opt/repo-sftp-host-fingerprint + ), // opt/repo-sftp-host-fingerprint + ), // opt/repo-sftp-host-fingerprint + ), // opt/repo-sftp-host-fingerprint + ), // opt/repo-sftp-host-fingerprint + // ----------------------------------------------------------------------------------------------------------------------------- + PARSE_RULE_OPTION // opt/repo-sftp-host-key-hash-type + ( // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_NAME("repo-sftp-host-key-hash-type"), // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_TYPE(cfgOptTypeStringId), // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_RESET(true), // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_REQUIRED(true), // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_SECTION(cfgSectionGlobal), // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_GROUP_MEMBER(true), // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_GROUP_ID(cfgOptGrpRepo), // opt/repo-sftp-host-key-hash-type + // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND_ROLE_MAIN_VALID_LIST // opt/repo-sftp-host-key-hash-type + ( // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdAnnotate) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdBackup) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdCheck) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdExpire) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdInfo) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdManifest) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoCreate) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoGet) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoLs) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoPut) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoRm) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaCreate) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaDelete) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaUpgrade) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-host-key-hash-type + ), // opt/repo-sftp-host-key-hash-type + // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND_ROLE_ASYNC_VALID_LIST // opt/repo-sftp-host-key-hash-type + ( // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-host-key-hash-type + ), // opt/repo-sftp-host-key-hash-type + // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND_ROLE_LOCAL_VALID_LIST // opt/repo-sftp-host-key-hash-type + ( // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdBackup) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-host-key-hash-type + ), // opt/repo-sftp-host-key-hash-type + // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND_ROLE_REMOTE_VALID_LIST // opt/repo-sftp-host-key-hash-type + ( // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdAnnotate) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdCheck) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdInfo) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdManifest) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoCreate) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoGet) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoLs) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoPut) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoRm) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaCreate) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaDelete) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaUpgrade) // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-host-key-hash-type + ), // opt/repo-sftp-host-key-hash-type + // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTIONAL // opt/repo-sftp-host-key-hash-type + ( // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTIONAL_GROUP // opt/repo-sftp-host-key-hash-type + ( // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTIONAL_DEPEND // opt/repo-sftp-host-key-hash-type + ( // opt/repo-sftp-host-key-hash-type + PARSE_RULE_VAL_OPT(cfgOptRepoType), // opt/repo-sftp-host-key-hash-type + PARSE_RULE_VAL_STRID(parseRuleValStrIdSftp), // opt/repo-sftp-host-key-hash-type + ), // opt/repo-sftp-host-key-hash-type + // opt/repo-sftp-host-key-hash-type + PARSE_RULE_OPTIONAL_ALLOW_LIST // opt/repo-sftp-host-key-hash-type + ( // opt/repo-sftp-host-key-hash-type + PARSE_RULE_VAL_STRID(parseRuleValStrIdMd5), // opt/repo-sftp-host-key-hash-type + PARSE_RULE_VAL_STRID(parseRuleValStrIdSha1), // opt/repo-sftp-host-key-hash-type + PARSE_RULE_VAL_STRID(parseRuleValStrIdSha256), // opt/repo-sftp-host-key-hash-type + ), // opt/repo-sftp-host-key-hash-type + ), // opt/repo-sftp-host-key-hash-type + ), // opt/repo-sftp-host-key-hash-type + ), // opt/repo-sftp-host-key-hash-type + // ----------------------------------------------------------------------------------------------------------------------------- + PARSE_RULE_OPTION // opt/repo-sftp-host-port + ( // opt/repo-sftp-host-port + PARSE_RULE_OPTION_NAME("repo-sftp-host-port"), // opt/repo-sftp-host-port + PARSE_RULE_OPTION_TYPE(cfgOptTypeInteger), // opt/repo-sftp-host-port + PARSE_RULE_OPTION_RESET(true), // opt/repo-sftp-host-port + PARSE_RULE_OPTION_REQUIRED(true), // opt/repo-sftp-host-port + PARSE_RULE_OPTION_SECTION(cfgSectionGlobal), // opt/repo-sftp-host-port + PARSE_RULE_OPTION_GROUP_MEMBER(true), // opt/repo-sftp-host-port + PARSE_RULE_OPTION_GROUP_ID(cfgOptGrpRepo), // opt/repo-sftp-host-port + // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND_ROLE_MAIN_VALID_LIST // opt/repo-sftp-host-port + ( // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdAnnotate) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdBackup) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdCheck) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdExpire) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdInfo) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdManifest) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoCreate) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoGet) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoLs) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoPut) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoRm) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaCreate) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaDelete) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaUpgrade) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-host-port + ), // opt/repo-sftp-host-port + // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND_ROLE_ASYNC_VALID_LIST // opt/repo-sftp-host-port + ( // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-host-port + ), // opt/repo-sftp-host-port + // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND_ROLE_LOCAL_VALID_LIST // opt/repo-sftp-host-port + ( // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdBackup) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-host-port + ), // opt/repo-sftp-host-port + // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND_ROLE_REMOTE_VALID_LIST // opt/repo-sftp-host-port + ( // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdAnnotate) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdCheck) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdInfo) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdManifest) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoCreate) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoGet) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoLs) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoPut) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoRm) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaCreate) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaDelete) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaUpgrade) // opt/repo-sftp-host-port + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-host-port + ), // opt/repo-sftp-host-port + // opt/repo-sftp-host-port + PARSE_RULE_OPTIONAL // opt/repo-sftp-host-port + ( // opt/repo-sftp-host-port + PARSE_RULE_OPTIONAL_GROUP // opt/repo-sftp-host-port + ( // opt/repo-sftp-host-port + PARSE_RULE_OPTIONAL_DEPEND // opt/repo-sftp-host-port + ( // opt/repo-sftp-host-port + PARSE_RULE_VAL_OPT(cfgOptRepoType), // opt/repo-sftp-host-port + PARSE_RULE_VAL_STRID(parseRuleValStrIdSftp), // opt/repo-sftp-host-port + ), // opt/repo-sftp-host-port + // opt/repo-sftp-host-port + PARSE_RULE_OPTIONAL_ALLOW_RANGE // opt/repo-sftp-host-port + ( // opt/repo-sftp-host-port + PARSE_RULE_VAL_INT(parseRuleValInt1), // opt/repo-sftp-host-port + PARSE_RULE_VAL_INT(parseRuleValInt65535), // opt/repo-sftp-host-port + ), // opt/repo-sftp-host-port + // opt/repo-sftp-host-port + PARSE_RULE_OPTIONAL_DEFAULT // opt/repo-sftp-host-port + ( // opt/repo-sftp-host-port + PARSE_RULE_VAL_INT(parseRuleValInt22), // opt/repo-sftp-host-port + PARSE_RULE_VAL_STR(parseRuleValStrQT_22_QT), // opt/repo-sftp-host-port + ), // opt/repo-sftp-host-port + ), // opt/repo-sftp-host-port + ), // opt/repo-sftp-host-port + ), // opt/repo-sftp-host-port + // ----------------------------------------------------------------------------------------------------------------------------- + PARSE_RULE_OPTION // opt/repo-sftp-host-user + ( // opt/repo-sftp-host-user + PARSE_RULE_OPTION_NAME("repo-sftp-host-user"), // opt/repo-sftp-host-user + PARSE_RULE_OPTION_TYPE(cfgOptTypeString), // opt/repo-sftp-host-user + PARSE_RULE_OPTION_RESET(true), // opt/repo-sftp-host-user + PARSE_RULE_OPTION_REQUIRED(true), // opt/repo-sftp-host-user + PARSE_RULE_OPTION_SECTION(cfgSectionGlobal), // opt/repo-sftp-host-user + PARSE_RULE_OPTION_GROUP_MEMBER(true), // opt/repo-sftp-host-user + PARSE_RULE_OPTION_GROUP_ID(cfgOptGrpRepo), // opt/repo-sftp-host-user + // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND_ROLE_MAIN_VALID_LIST // opt/repo-sftp-host-user + ( // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdAnnotate) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdBackup) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdCheck) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdExpire) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdInfo) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdManifest) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoCreate) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoGet) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoLs) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoPut) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoRm) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaCreate) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaDelete) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaUpgrade) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-host-user + ), // opt/repo-sftp-host-user + // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND_ROLE_ASYNC_VALID_LIST // opt/repo-sftp-host-user + ( // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-host-user + ), // opt/repo-sftp-host-user + // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND_ROLE_LOCAL_VALID_LIST // opt/repo-sftp-host-user + ( // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdBackup) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-host-user + ), // opt/repo-sftp-host-user + // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND_ROLE_REMOTE_VALID_LIST // opt/repo-sftp-host-user + ( // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdAnnotate) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdCheck) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdInfo) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdManifest) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoCreate) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoGet) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoLs) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoPut) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoRm) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaCreate) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaDelete) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaUpgrade) // opt/repo-sftp-host-user + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-host-user + ), // opt/repo-sftp-host-user + // opt/repo-sftp-host-user + PARSE_RULE_OPTIONAL // opt/repo-sftp-host-user + ( // opt/repo-sftp-host-user + PARSE_RULE_OPTIONAL_GROUP // opt/repo-sftp-host-user + ( // opt/repo-sftp-host-user + PARSE_RULE_OPTIONAL_DEPEND // opt/repo-sftp-host-user + ( // opt/repo-sftp-host-user + PARSE_RULE_VAL_OPT(cfgOptRepoType), // opt/repo-sftp-host-user + PARSE_RULE_VAL_STRID(parseRuleValStrIdSftp), // opt/repo-sftp-host-user + ), // opt/repo-sftp-host-user + ), // opt/repo-sftp-host-user + ), // opt/repo-sftp-host-user + ), // opt/repo-sftp-host-user + // ----------------------------------------------------------------------------------------------------------------------------- + PARSE_RULE_OPTION // opt/repo-sftp-private-key-file + ( // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_NAME("repo-sftp-private-key-file"), // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_TYPE(cfgOptTypeString), // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_RESET(true), // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_REQUIRED(true), // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_SECTION(cfgSectionGlobal), // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_GROUP_MEMBER(true), // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_GROUP_ID(cfgOptGrpRepo), // opt/repo-sftp-private-key-file + // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND_ROLE_MAIN_VALID_LIST // opt/repo-sftp-private-key-file + ( // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdAnnotate) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdBackup) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdCheck) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdExpire) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdInfo) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdManifest) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoCreate) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoGet) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoLs) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoPut) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoRm) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaCreate) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaDelete) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaUpgrade) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-private-key-file + ), // opt/repo-sftp-private-key-file + // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND_ROLE_ASYNC_VALID_LIST // opt/repo-sftp-private-key-file + ( // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-private-key-file + ), // opt/repo-sftp-private-key-file + // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND_ROLE_LOCAL_VALID_LIST // opt/repo-sftp-private-key-file + ( // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdBackup) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-private-key-file + ), // opt/repo-sftp-private-key-file + // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND_ROLE_REMOTE_VALID_LIST // opt/repo-sftp-private-key-file + ( // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdAnnotate) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdCheck) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdInfo) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdManifest) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoCreate) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoGet) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoLs) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoPut) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoRm) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaCreate) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaDelete) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaUpgrade) // opt/repo-sftp-private-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-private-key-file + ), // opt/repo-sftp-private-key-file + // opt/repo-sftp-private-key-file + PARSE_RULE_OPTIONAL // opt/repo-sftp-private-key-file + ( // opt/repo-sftp-private-key-file + PARSE_RULE_OPTIONAL_GROUP // opt/repo-sftp-private-key-file + ( // opt/repo-sftp-private-key-file + PARSE_RULE_OPTIONAL_DEPEND // opt/repo-sftp-private-key-file + ( // opt/repo-sftp-private-key-file + PARSE_RULE_VAL_OPT(cfgOptRepoType), // opt/repo-sftp-private-key-file + PARSE_RULE_VAL_STRID(parseRuleValStrIdSftp), // opt/repo-sftp-private-key-file + ), // opt/repo-sftp-private-key-file + ), // opt/repo-sftp-private-key-file + ), // opt/repo-sftp-private-key-file + ), // opt/repo-sftp-private-key-file + // ----------------------------------------------------------------------------------------------------------------------------- + PARSE_RULE_OPTION // opt/repo-sftp-private-key-passphrase + ( // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_NAME("repo-sftp-private-key-passphrase"), // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_TYPE(cfgOptTypeString), // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_RESET(true), // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_REQUIRED(false), // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_SECTION(cfgSectionGlobal), // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_SECURE(true), // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_GROUP_MEMBER(true), // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_GROUP_ID(cfgOptGrpRepo), // opt/repo-sftp-private-key-passphrase + // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND_ROLE_MAIN_VALID_LIST // opt/repo-sftp-private-key-passphrase + ( // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdAnnotate) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdBackup) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdCheck) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdExpire) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdInfo) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdManifest) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoCreate) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoGet) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoLs) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoPut) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoRm) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaCreate) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaDelete) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaUpgrade) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-private-key-passphrase + ), // opt/repo-sftp-private-key-passphrase + // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND_ROLE_ASYNC_VALID_LIST // opt/repo-sftp-private-key-passphrase + ( // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-private-key-passphrase + ), // opt/repo-sftp-private-key-passphrase + // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND_ROLE_LOCAL_VALID_LIST // opt/repo-sftp-private-key-passphrase + ( // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdBackup) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-private-key-passphrase + ), // opt/repo-sftp-private-key-passphrase + // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND_ROLE_REMOTE_VALID_LIST // opt/repo-sftp-private-key-passphrase + ( // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdAnnotate) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdCheck) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdInfo) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdManifest) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoCreate) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoGet) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoLs) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoPut) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoRm) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaCreate) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaDelete) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaUpgrade) // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-private-key-passphrase + ), // opt/repo-sftp-private-key-passphrase + // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTIONAL // opt/repo-sftp-private-key-passphrase + ( // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTIONAL_GROUP // opt/repo-sftp-private-key-passphrase + ( // opt/repo-sftp-private-key-passphrase + PARSE_RULE_OPTIONAL_DEPEND // opt/repo-sftp-private-key-passphrase + ( // opt/repo-sftp-private-key-passphrase + PARSE_RULE_VAL_OPT(cfgOptRepoType), // opt/repo-sftp-private-key-passphrase + PARSE_RULE_VAL_STRID(parseRuleValStrIdSftp), // opt/repo-sftp-private-key-passphrase + ), // opt/repo-sftp-private-key-passphrase + ), // opt/repo-sftp-private-key-passphrase + ), // opt/repo-sftp-private-key-passphrase + ), // opt/repo-sftp-private-key-passphrase + // ----------------------------------------------------------------------------------------------------------------------------- + PARSE_RULE_OPTION // opt/repo-sftp-public-key-file + ( // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_NAME("repo-sftp-public-key-file"), // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_TYPE(cfgOptTypeString), // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_RESET(true), // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_REQUIRED(false), // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_SECTION(cfgSectionGlobal), // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_GROUP_MEMBER(true), // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_GROUP_ID(cfgOptGrpRepo), // opt/repo-sftp-public-key-file + // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND_ROLE_MAIN_VALID_LIST // opt/repo-sftp-public-key-file + ( // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdAnnotate) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdBackup) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdCheck) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdExpire) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdInfo) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdManifest) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoCreate) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoGet) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoLs) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoPut) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoRm) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaCreate) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaDelete) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaUpgrade) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-public-key-file + ), // opt/repo-sftp-public-key-file + // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND_ROLE_ASYNC_VALID_LIST // opt/repo-sftp-public-key-file + ( // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-public-key-file + ), // opt/repo-sftp-public-key-file + // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND_ROLE_LOCAL_VALID_LIST // opt/repo-sftp-public-key-file + ( // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdBackup) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-public-key-file + ), // opt/repo-sftp-public-key-file + // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND_ROLE_REMOTE_VALID_LIST // opt/repo-sftp-public-key-file + ( // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdAnnotate) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdArchiveGet) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdArchivePush) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdCheck) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdInfo) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdManifest) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoCreate) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoGet) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoLs) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoPut) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRepoRm) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdRestore) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaCreate) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaDelete) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdStanzaUpgrade) // opt/repo-sftp-public-key-file + PARSE_RULE_OPTION_COMMAND(cfgCmdVerify) // opt/repo-sftp-public-key-file + ), // opt/repo-sftp-public-key-file + // opt/repo-sftp-public-key-file + PARSE_RULE_OPTIONAL // opt/repo-sftp-public-key-file + ( // opt/repo-sftp-public-key-file + PARSE_RULE_OPTIONAL_GROUP // opt/repo-sftp-public-key-file + ( // opt/repo-sftp-public-key-file + PARSE_RULE_OPTIONAL_DEPEND // opt/repo-sftp-public-key-file + ( // opt/repo-sftp-public-key-file + PARSE_RULE_VAL_OPT(cfgOptRepoType), // opt/repo-sftp-public-key-file + PARSE_RULE_VAL_STRID(parseRuleValStrIdSftp), // opt/repo-sftp-public-key-file + ), // opt/repo-sftp-public-key-file + ), // opt/repo-sftp-public-key-file + ), // opt/repo-sftp-public-key-file + ), // opt/repo-sftp-public-key-file + // ----------------------------------------------------------------------------------------------------------------------------- PARSE_RULE_OPTION // opt/repo-storage-ca-file ( // opt/repo-storage-ca-file PARSE_RULE_OPTION_NAME("repo-storage-ca-file"), // opt/repo-storage-ca-file @@ -8450,6 +9122,7 @@ static const ParseRuleOption parseRuleOption[CFG_OPTION_TOTAL] = PARSE_RULE_VAL_STRID(parseRuleValStrIdGcs), // opt/repo-type PARSE_RULE_VAL_STRID(parseRuleValStrIdPosix), // opt/repo-type PARSE_RULE_VAL_STRID(parseRuleValStrIdS3), // opt/repo-type + PARSE_RULE_VAL_STRID(parseRuleValStrIdSftp), // opt/repo-type ), // opt/repo-type // opt/repo-type PARSE_RULE_OPTIONAL_DEFAULT // opt/repo-type @@ -9990,6 +10663,14 @@ static const uint8_t optionResolveOrder[] = cfgOptRepoS3Role, // opt-resolve-order cfgOptRepoS3Token, // opt-resolve-order cfgOptRepoS3UriStyle, // opt-resolve-order + cfgOptRepoSftpHost, // opt-resolve-order + cfgOptRepoSftpHostFingerprint, // opt-resolve-order + cfgOptRepoSftpHostKeyHashType, // opt-resolve-order + cfgOptRepoSftpHostPort, // opt-resolve-order + cfgOptRepoSftpHostUser, // opt-resolve-order + cfgOptRepoSftpPrivateKeyFile, // opt-resolve-order + cfgOptRepoSftpPrivateKeyPassphrase, // opt-resolve-order + cfgOptRepoSftpPublicKeyFile, // opt-resolve-order cfgOptRepoStorageCaFile, // opt-resolve-order cfgOptRepoStorageCaPath, // opt-resolve-order cfgOptRepoStorageHost, // opt-resolve-order diff --git a/src/configure b/src/configure index 38cbb2b1c..4ebc9925b 100755 --- a/src/configure +++ b/src/configure @@ -4034,6 +4034,105 @@ fi fi +# Check optional libSSH2 library +# ---------------------------------------------------------------------------------------------------------------------------------- +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for libssh2_init in -lssh2" >&5 +printf %s "checking for libssh2_init in -lssh2... " >&6; } +if test ${ac_cv_lib_ssh2_libssh2_init+y} +then : + printf %s "(cached) " >&6 +else $as_nop + ac_check_lib_save_LIBS=$LIBS +LIBS="-lssh2 $LIBS" +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +char libssh2_init (); +int +main (void) +{ +return libssh2_init (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO" +then : + ac_cv_lib_ssh2_libssh2_init=yes +else $as_nop + ac_cv_lib_ssh2_libssh2_init=no +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_ssh2_libssh2_init" >&5 +printf "%s\n" "$ac_cv_lib_ssh2_libssh2_init" >&6; } +if test "x$ac_cv_lib_ssh2_libssh2_init" = xyes +then : + ac_fn_c_check_header_compile "$LINENO" "libssh2.h" "ac_cv_header_libssh2_h" "$ac_includes_default" +if test "x$ac_cv_header_libssh2_h" = xyes +then : + printf "%s\n" "#define HAVE_LIBSSH2 1" >>confdefs.h + LIBS="${LIBS} -lssh2" + +else $as_nop + as_fn_error $? "header file is required" "$LINENO" 5 +fi + +fi + +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for libssh2_sftp_init in -lssh2" >&5 +printf %s "checking for libssh2_sftp_init in -lssh2... " >&6; } +if test ${ac_cv_lib_ssh2_libssh2_sftp_init+y} +then : + printf %s "(cached) " >&6 +else $as_nop + ac_check_lib_save_LIBS=$LIBS +LIBS="-lssh2 $LIBS" +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +char libssh2_sftp_init (); +int +main (void) +{ +return libssh2_sftp_init (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO" +then : + ac_cv_lib_ssh2_libssh2_sftp_init=yes +else $as_nop + ac_cv_lib_ssh2_libssh2_sftp_init=no +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_ssh2_libssh2_sftp_init" >&5 +printf "%s\n" "$ac_cv_lib_ssh2_libssh2_sftp_init" >&6; } +if test "x$ac_cv_lib_ssh2_libssh2_sftp_init" = xyes +then : + ac_fn_c_check_header_compile "$LINENO" "libssh2_sftp.h" "ac_cv_header_libssh2_sftp_h" "$ac_includes_default" +if test "x$ac_cv_header_libssh2_sftp_h" = xyes +then : + +else $as_nop + as_fn_error $? "header file is required" "$LINENO" 5 +fi + +fi + + # Check optional zst library. Ignore any versions below 1.0. # ---------------------------------------------------------------------------------------------------------------------------------- { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for ZSTD_isError in -lzstd" >&5 @@ -5599,4 +5698,4 @@ if test -n "$ac_unrecognized_opts" && test "$enable_option_checking" != no; then printf "%s\n" "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2;} fi -# Generated from src/build/configure.ac sha1 5bd14429291b37c7b69a19b6086863c15530e138 +# Generated from src/build/configure.ac sha1 60bcd5099f8e4bfc1e900636b46fc25312c9729f diff --git a/src/main.c b/src/main.c index 1781be3a2..4ba534d2d 100644 --- a/src/main.c +++ b/src/main.c @@ -47,6 +47,7 @@ Main #include "storage/gcs/helper.h" #include "storage/helper.h" #include "storage/s3/helper.h" +#include "storage/sftp/helper.h" #include "version.h" /*********************************************************************************************************************************** @@ -68,6 +69,9 @@ main(int argListSize, const char *argList[]) STORAGE_CIFS_HELPER, STORAGE_GCS_HELPER, STORAGE_S3_HELPER, +#ifdef HAVE_LIBSSH2 + STORAGE_SFTP_HELPER, +#endif STORAGE_END_HELPER }; diff --git a/src/meson.build b/src/meson.build index 80e62ac7c..88f4b6cb2 100644 --- a/src/meson.build +++ b/src/meson.build @@ -253,6 +253,10 @@ src_pgbackrest = [ 'storage/s3/read.c', 'storage/s3/storage.c', 'storage/s3/write.c', + 'storage/sftp/helper.c', + 'storage/sftp/read.c', + 'storage/sftp/storage.c', + 'storage/sftp/write.c', 'main.c', ] @@ -268,6 +272,7 @@ executable( lib_openssl, lib_lz4, lib_pq, + lib_ssh2, lib_xml, lib_z, lib_zstd, diff --git a/src/storage/sftp/helper.c b/src/storage/sftp/helper.c new file mode 100644 index 000000000..187c602c7 --- /dev/null +++ b/src/storage/sftp/helper.c @@ -0,0 +1,38 @@ +/*********************************************************************************************************************************** +SFTP Storage Helper +***********************************************************************************************************************************/ +#include "build.auto.h" + +#ifdef HAVE_LIBSSH2 + +#include "common/debug.h" +#include "common/log.h" +#include "config/config.h" +#include "storage/sftp/helper.h" + +/**********************************************************************************************************************************/ +FN_EXTERN Storage * +storageSftpHelper(const unsigned int repoIdx, const bool write, StoragePathExpressionCallback pathExpressionCallback) +{ + FUNCTION_LOG_BEGIN(logLevelDebug); + FUNCTION_LOG_PARAM(UINT, repoIdx); + FUNCTION_LOG_PARAM(BOOL, write); + FUNCTION_LOG_PARAM_P(VOID, pathExpressionCallback); + FUNCTION_LOG_END(); + + ASSERT(cfgOptionIdxStrId(cfgOptRepoType, repoIdx) == STORAGE_SFTP_TYPE); + + FUNCTION_LOG_RETURN( + STORAGE, + storageSftpNewP( + cfgOptionIdxStr(cfgOptRepoPath, repoIdx), cfgOptionIdxStr(cfgOptRepoSftpHost, repoIdx), + cfgOptionIdxUInt(cfgOptRepoSftpHostPort, repoIdx), cfgOptionIdxStr(cfgOptRepoSftpHostUser, repoIdx), + cfgOptionUInt64(cfgOptIoTimeout), cfgOptionIdxStr(cfgOptRepoSftpPrivateKeyFile, repoIdx), + cfgOptionIdxStrId(cfgOptRepoSftpHostKeyHashType, repoIdx), .write = write, + .pathExpressionFunction = pathExpressionCallback, .modeFile = STORAGE_MODE_FILE_DEFAULT, + .modePath = STORAGE_MODE_PATH_DEFAULT, .keyPub = cfgOptionIdxStrNull(cfgOptRepoSftpPublicKeyFile, repoIdx), + .keyPassphrase = cfgOptionIdxStrNull(cfgOptRepoSftpPrivateKeyPassphrase, repoIdx), + .hostFingerprint = cfgOptionIdxStrNull(cfgOptRepoSftpHostFingerprint, repoIdx))); +} + +#endif // HAVE_LIBSSH2 diff --git a/src/storage/sftp/helper.h b/src/storage/sftp/helper.h new file mode 100644 index 000000000..eab5307bb --- /dev/null +++ b/src/storage/sftp/helper.h @@ -0,0 +1,23 @@ +/*********************************************************************************************************************************** +SFTP Storage Helper +***********************************************************************************************************************************/ +#ifndef STORAGE_SFTP_STORAGE_HELPER_H +#define STORAGE_SFTP_STORAGE_HELPER_H + +#ifdef HAVE_LIBSSH2 + +#include "storage/sftp/storage.h" + +/*********************************************************************************************************************************** +Functions +***********************************************************************************************************************************/ +FN_EXTERN Storage *storageSftpHelper(unsigned int repoIdx, bool write, StoragePathExpressionCallback pathExpressionCallback); + +/*********************************************************************************************************************************** +Storage helper for StorageHelper array passed to storageHelperInit() +***********************************************************************************************************************************/ +#define STORAGE_SFTP_HELPER {.type = STORAGE_SFTP_TYPE, .helper = storageSftpHelper} + +#endif // HAVE_LIBSSH2 + +#endif diff --git a/src/storage/sftp/read.c b/src/storage/sftp/read.c new file mode 100644 index 000000000..17285cfb1 --- /dev/null +++ b/src/storage/sftp/read.c @@ -0,0 +1,300 @@ +/*********************************************************************************************************************************** +SFTP Storage Read +***********************************************************************************************************************************/ +#include "build.auto.h" + +#ifdef HAVE_LIBSSH2 + +#include "common/debug.h" +#include "common/io/session.h" +#include "common/log.h" +#include "common/wait.h" +#include "storage/read.intern.h" +#include "storage/sftp/read.h" + +/*********************************************************************************************************************************** +Object types +***********************************************************************************************************************************/ +typedef struct StorageReadSftp +{ + StorageReadInterface interface; // Interface + StorageSftp *storage; // Storage that created this object + + IoSession *ioSession; // IoSession (socket) connection to SFTP server + LIBSSH2_SESSION *session; // LibSsh2 session + LIBSSH2_SFTP *sftpSession; // LibSsh2 session sftp session + LIBSSH2_SFTP_HANDLE *sftpHandle; // LibSsh2 session sftp handle + LIBSSH2_SFTP_ATTRIBUTES *attr; // LibSsh2 file attributes + uint64_t current; // Current bytes read from file + uint64_t limit; // Limit bytes to be read from file (UINT64_MAX for no limit) + bool eof; // Did we reach end of file + TimeMSec timeout; // Session timeout +} StorageReadSftp; + +/*********************************************************************************************************************************** +Macros for function logging +***********************************************************************************************************************************/ +#define FUNCTION_LOG_STORAGE_READ_SFTP_TYPE \ + StorageReadSftp * +#define FUNCTION_LOG_STORAGE_READ_SFTP_FORMAT(value, buffer, bufferSize) \ + objNameToLog(value, "StorageReadSftp", buffer, bufferSize) + +/*********************************************************************************************************************************** +Open the file +***********************************************************************************************************************************/ +static bool +storageReadSftpOpen(THIS_VOID) +{ + THIS(StorageReadSftp); + + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(STORAGE_READ_SFTP, this); + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + + // Open the file + Wait *const wait = waitNew(this->timeout); + + do + { + this->sftpHandle = libssh2_sftp_open_ex( + this->sftpSession, strZ(this->interface.name), (unsigned int)strSize(this->interface.name), LIBSSH2_FXF_READ, 0, + LIBSSH2_SFTP_OPENFILE); + } + while (this->sftpHandle == NULL && waitMore(wait)); + + waitFree(wait); + + if (this->sftpHandle == NULL) + { + int rc = libssh2_session_last_errno(this->session); + + if (rc == LIBSSH2_ERROR_SFTP_PROTOCOL || rc == LIBSSH2_ERROR_EAGAIN) + { + if (libssh2_sftp_last_error(this->sftpSession) == LIBSSH2_FX_NO_SUCH_FILE) + { + if (!this->interface.ignoreMissing) + THROW_FMT(FileMissingError, STORAGE_ERROR_READ_MISSING, strZ(this->interface.name)); + } + else + THROW_FMT(FileOpenError, STORAGE_ERROR_READ_OPEN, strZ(this->interface.name)); + } + } + // Else success + else + { + // Seek to offset, libssh2_sftp_seek64 returns void + if (this->interface.offset != 0) + libssh2_sftp_seek64(this->sftpHandle, this->interface.offset); + } + + FUNCTION_LOG_RETURN(BOOL, this->sftpHandle != NULL); +} + +/*********************************************************************************************************************************** +Read from a file +***********************************************************************************************************************************/ +static size_t +storageReadSftp(THIS_VOID, Buffer *const buffer, const bool block) +{ + THIS(StorageReadSftp); + + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(STORAGE_READ_SFTP, this); + FUNCTION_LOG_PARAM(BUFFER, buffer); + FUNCTION_LOG_PARAM(BOOL, block); + FUNCTION_LOG_END(); + + ASSERT(this != NULL && this->sftpHandle != NULL); + ASSERT(buffer != NULL && !bufFull(buffer)); + + ssize_t actualBytes = 0; + + // Read if EOF has not been reached + if (!this->eof) + { + // Determine expected bytes to read. If remaining size in the buffer would exceed the limit then reduce the expected read. + size_t expectedBytes = bufRemains(buffer); + + if (this->current + expectedBytes > this->limit) + expectedBytes = (size_t)(this->limit - this->current); + + bufLimitSet(buffer, expectedBytes); + ssize_t rc = 0; + + // Read until EOF or buffer is full + do + { + Wait *const wait = waitNew(this->timeout); + + do + { + rc = libssh2_sftp_read(this->sftpHandle, (char *)bufRemainsPtr(buffer), bufRemains(buffer)); + } + while (rc == LIBSSH2_ERROR_EAGAIN && waitMore(wait)); + + waitFree(wait); + + // Break on EOF or error + if (rc <= 0) + break; + + // Account/shift for bytes read + bufUsedInc(buffer, (size_t)rc); + } + while (!bufFull(buffer)); + + // Total bytes read into the buffer + actualBytes = (ssize_t)bufUsed(buffer); + + // Error occurred during read + if (rc < 0) + { + if (rc == LIBSSH2_ERROR_SFTP_PROTOCOL) + { + uint64_t sftpErr = 0; + + // libssh2 sftp lseek seems to return LIBSSH2_FX_BAD_MESSAGE on a seek too far + if ((sftpErr = libssh2_sftp_last_error(this->sftpSession)) == LIBSSH2_FX_BAD_MESSAGE && this->interface.offset > 0) + THROW_FMT(FileOpenError, STORAGE_ERROR_READ_SEEK, this->interface.offset, strZ(this->interface.name)); + else + THROW_FMT(FileReadError, "unable to read '%s': sftp errno [%" PRIu64 "]", strZ(this->interface.name), sftpErr); + } + else + THROW_FMT(FileReadError, "unable to read '%s'", strZ(this->interface.name)); + } + + // Update amount of buffer used + this->current += (uint64_t)actualBytes; + + // If less data than expected was read or the limit has been reached then EOF. The file may not actually be EOF but we are + // not concerned with files that are growing. Just read up to the point where the file is being extended. + if ((size_t)actualBytes != expectedBytes || this->current == this->limit) + this->eof = true; + } + + FUNCTION_LOG_RETURN(SIZE, (size_t)actualBytes); +} + +/*********************************************************************************************************************************** +Close the file +***********************************************************************************************************************************/ +static void +storageReadSftpClose(THIS_VOID) +{ + THIS(StorageReadSftp); + + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(STORAGE_READ_SFTP, this); + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + + if (this->sftpHandle != NULL) + { + int rc = 0; + Wait *const wait = waitNew(this->timeout); + + // Close the file + do + { + rc = libssh2_sftp_close(this->sftpHandle); + } + while (rc == LIBSSH2_ERROR_EAGAIN && waitMore(wait)); + + waitFree(wait); + + if (rc) + { + THROW_FMT( + FileCloseError, + STORAGE_ERROR_READ_CLOSE ": libssh2 errno [%d]%s", strZ(this->interface.name), rc, + rc == LIBSSH2_ERROR_SFTP_PROTOCOL ? + strZ(strNewFmt(": sftp errno [%lu]", libssh2_sftp_last_error(this->sftpSession))) : ""); + } + } + + this->sftpHandle = NULL; + + FUNCTION_LOG_RETURN_VOID(); +} + +/*********************************************************************************************************************************** +Has file reached EOF? +***********************************************************************************************************************************/ +static bool +storageReadSftpEof(THIS_VOID) +{ + THIS(StorageReadSftp); + + FUNCTION_TEST_BEGIN(); + FUNCTION_TEST_PARAM(STORAGE_READ_SFTP, this); + FUNCTION_TEST_END(); + + ASSERT(this != NULL); + + FUNCTION_TEST_RETURN(BOOL, this->eof); +} + +/**********************************************************************************************************************************/ +FN_EXTERN StorageRead * +storageReadSftpNew( + StorageSftp *const storage, const String *const name, const bool ignoreMissing, IoSession *const ioSession, + LIBSSH2_SESSION *const session, LIBSSH2_SFTP *const sftpSession, LIBSSH2_SFTP_HANDLE *const sftpHandle, + const TimeMSec timeout, const uint64_t offset, const Variant *const limit) +{ + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(STRING, name); + FUNCTION_LOG_PARAM(BOOL, ignoreMissing); + FUNCTION_LOG_PARAM(IO_SESSION, ioSession); + FUNCTION_LOG_PARAM_P(VOID, session); + FUNCTION_LOG_PARAM_P(VOID, sftpSession); + FUNCTION_LOG_PARAM_P(VOID, sftpHandle); + FUNCTION_LOG_PARAM(TIME_MSEC, timeout); + FUNCTION_LOG_PARAM(UINT64, offset); + FUNCTION_LOG_PARAM(VARIANT, limit); + FUNCTION_LOG_END(); + + ASSERT(name != NULL); + + OBJ_NEW_BEGIN(StorageReadSftp, .childQty = MEM_CONTEXT_QTY_MAX) + { + *this = (StorageReadSftp) + { + .storage = storage, + .ioSession = ioSession, + .session = session, + .sftpSession = sftpSession, + .sftpHandle = sftpHandle, + .timeout = timeout, + + // Rather than enable/disable limit checking just use a big number when there is no limit. We can feel pretty confident + // that no files will be > UINT64_MAX in size. This is a copy of the interface limit but it simplifies the code during + // read so it seems worthwhile. + .limit = limit == NULL ? UINT64_MAX : varUInt64(limit), + + .interface = (StorageReadInterface) + { + .type = STORAGE_SFTP_TYPE, + .name = strDup(name), + .ignoreMissing = ignoreMissing, + .offset = offset, + .limit = varDup(limit), + + .ioInterface = (IoReadInterface) + { + .close = storageReadSftpClose, + .eof = storageReadSftpEof, + .open = storageReadSftpOpen, + .read = storageReadSftp, + }, + }, + }; + } + OBJ_NEW_END(); + + FUNCTION_LOG_RETURN(STORAGE_READ, storageReadNew(this, &this->interface)); +} + +#endif // HAVE_LIBSSH2 diff --git a/src/storage/sftp/read.h b/src/storage/sftp/read.h new file mode 100644 index 000000000..b5855c592 --- /dev/null +++ b/src/storage/sftp/read.h @@ -0,0 +1,17 @@ +/*********************************************************************************************************************************** +SFTP Storage Read +***********************************************************************************************************************************/ +#ifndef STORAGE_SFTP_READ_H +#define STORAGE_SFTP_READ_H + +#include "storage/read.h" +#include "storage/sftp/storage.intern.h" + +/*********************************************************************************************************************************** +Constructors +***********************************************************************************************************************************/ +FN_EXTERN StorageRead *storageReadSftpNew( + StorageSftp *storage, const String *name, bool ignoreMissing, IoSession *ioSession, LIBSSH2_SESSION *session, + LIBSSH2_SFTP *sftpSession, LIBSSH2_SFTP_HANDLE *sftpHandle, TimeMSec timeout, uint64_t offset, const Variant *limit); + +#endif diff --git a/src/storage/sftp/storage.c b/src/storage/sftp/storage.c new file mode 100644 index 000000000..ef7a1542a --- /dev/null +++ b/src/storage/sftp/storage.c @@ -0,0 +1,891 @@ +/*********************************************************************************************************************************** +SFTP Storage +***********************************************************************************************************************************/ +#include "build.auto.h" + +#ifdef HAVE_LIBSSH2 + +#include "common/crypto/hash.h" +#include "common/debug.h" +#include "common/io/socket/client.h" +#include "common/log.h" +#include "common/user.h" +#include "common/wait.h" +#include "storage/sftp/read.h" +#include "storage/sftp/storage.intern.h" +#include "storage/sftp/write.h" + +/*********************************************************************************************************************************** +Define PATH_MAX if it is not defined +***********************************************************************************************************************************/ +#ifndef PATH_MAX +#define PATH_MAX (4 * 1024) +#endif + +/*********************************************************************************************************************************** +Object type +***********************************************************************************************************************************/ +struct StorageSftp +{ + STORAGE_COMMON_MEMBER; + + IoSession *ioSession; // IoSession (socket) connection to SFTP server + LIBSSH2_SESSION *session; // LibSsh2 session + LIBSSH2_SFTP *sftpSession; // LibSsh2 session sftp session + LIBSSH2_SFTP_HANDLE *sftpHandle; // LibSsh2 session sftp handle + TimeMSec timeout; // Session timeout +}; + +/*********************************************************************************************************************************** +Free libssh2 resources +***********************************************************************************************************************************/ +static void +storageSftpLibSsh2SessionFreeResource(THIS_VOID) +{ + THIS(StorageSftp); + + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(STORAGE_SFTP, this); + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + + int rc; + + if (this->sftpHandle != NULL) + { + do + { + rc = libssh2_sftp_close(this->sftpHandle); + } + while (rc == LIBSSH2_ERROR_EAGAIN); + + if (rc != 0) + { + THROW_FMT( + ServiceError, "failed to free resource sftpHandle: libssh2 errno [%d]%s", rc, + rc == LIBSSH2_ERROR_SFTP_PROTOCOL ? + strZ(strNewFmt(": sftp errno [%lu]", libssh2_sftp_last_error(this->sftpSession))) : ""); + } + } + + if (this->sftpSession != NULL) + { + do + { + rc = libssh2_sftp_shutdown(this->sftpSession); + } + while (rc == LIBSSH2_ERROR_EAGAIN); + + if (rc != 0) + { + THROW_FMT( + ServiceError, "failed to free resource sftpSession: libssh2 errno [%d]%s", rc, + rc == LIBSSH2_ERROR_SFTP_PROTOCOL ? + strZ(strNewFmt(": sftp errno [%lu]", libssh2_sftp_last_error(this->sftpSession))) : ""); + } + } + + if (this->session != NULL) + { + do + { + rc = libssh2_session_disconnect_ex(this->session, SSH_DISCONNECT_BY_APPLICATION, "pgbackrest instance shutdown", ""); + } + while (rc == LIBSSH2_ERROR_EAGAIN); + + if (rc != 0) + THROW_FMT(ServiceError, "failed to disconnect libssh2 session: libssh2 errno [%d]", rc); + + do + { + rc = libssh2_session_free(this->session); + } + while (rc == LIBSSH2_ERROR_EAGAIN); + + if (rc != 0) + THROW_FMT(ServiceError, "failed to free libssh2 session: libssh2 errno [%d]", rc); + } + + libssh2_exit(); + + FUNCTION_LOG_RETURN_VOID(); +} + +/**********************************************************************************************************************************/ +FN_EXTERN FN_NO_RETURN void +storageSftpEvalLibSsh2Error( + const int ssh2Errno, const uint64_t sftpErrno, const ErrorType *const errorType, const String *const message, + const String *const hint) +{ + FUNCTION_TEST_BEGIN(); + FUNCTION_TEST_PARAM(INT, ssh2Errno); + FUNCTION_TEST_PARAM(UINT64, sftpErrno); + FUNCTION_TEST_PARAM(ERROR_TYPE, errorType); + FUNCTION_TEST_PARAM(STRING, message); + FUNCTION_TEST_PARAM(STRING, hint); + FUNCTION_TEST_END(); + + ASSERT(errorType != NULL); + + THROWP_FMT( + errorType, "%slibssh2 error [%d]%s%s", message != NULL ? zNewFmt("%s: ", strZ(message)) : "", ssh2Errno, + ssh2Errno == LIBSSH2_ERROR_SFTP_PROTOCOL ? zNewFmt(": sftp error [%" PRIu64 "]", sftpErrno) : "", + hint != NULL ? zNewFmt("\n%s", strZ(hint)) : ""); + + FUNCTION_TEST_NO_RETURN(); +} + +/**********************************************************************************************************************************/ +static bool +storageSftpLibSsh2FxNoSuchFile(THIS_VOID, const int rc) +{ + THIS(StorageSftp); + + FUNCTION_TEST_BEGIN(); + FUNCTION_TEST_PARAM(STORAGE_SFTP, this); + FUNCTION_TEST_PARAM(INT, rc); + FUNCTION_TEST_END(); + + ASSERT(this != NULL); + + FUNCTION_TEST_RETURN( + BOOL, rc == LIBSSH2_ERROR_SFTP_PROTOCOL && libssh2_sftp_last_error(this->sftpSession) == LIBSSH2_FX_NO_SUCH_FILE); +} + +/**********************************************************************************************************************************/ +static StorageInfo +storageSftpInfo(THIS_VOID, const String *const file, const StorageInfoLevel level, const StorageInterfaceInfoParam param) +{ + THIS(StorageSftp); + + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(STORAGE_SFTP, this); + FUNCTION_LOG_PARAM(STRING, file); + FUNCTION_LOG_PARAM(ENUM, level); + FUNCTION_LOG_PARAM(BOOL, param.followLink); + FUNCTION_LOG_END(); + + FUNCTION_AUDIT_STRUCT(); + + ASSERT(this != NULL); + ASSERT(file != NULL); + + StorageInfo result = {.level = level}; + + // Stat the file to check if it exists + LIBSSH2_SFTP_ATTRIBUTES attr; + int rc; + Wait *const wait = waitNew(this->timeout); + + do + { + rc = libssh2_sftp_stat_ex( + this->sftpSession, strZ(file), (unsigned int)strSize(file), param.followLink ? LIBSSH2_SFTP_STAT : LIBSSH2_SFTP_LSTAT, + &attr); + } + while (rc == LIBSSH2_ERROR_EAGAIN && waitMore(wait)); + + waitFree(wait); + + // Throw libssh2 errors other than no such file + if (rc != 0) + { + if (!storageSftpLibSsh2FxNoSuchFile(this, rc)) + THROW_FMT(FileOpenError, STORAGE_ERROR_INFO, strZ(file)); + } + // Else the file exists + else + { + result.exists = true; + + // Add type info (no need set file type since it is the default) + if (result.level >= storageInfoLevelType && !LIBSSH2_SFTP_S_ISREG(attr.permissions)) + { + if (LIBSSH2_SFTP_S_ISDIR(attr.permissions)) + result.type = storageTypePath; + else if (LIBSSH2_SFTP_S_ISLNK(attr.permissions)) + result.type = storageTypeLink; + else + result.type = storageTypeSpecial; + } + + // Add basic level info + if (result.level >= storageInfoLevelBasic) + { + if ((attr.flags & LIBSSH2_SFTP_ATTR_ACMODTIME) != 0) + result.timeModified = (time_t)attr.mtime; + + if (result.type == storageTypeFile) + if ((attr.flags & LIBSSH2_SFTP_ATTR_SIZE) != 0) + result.size = (uint64_t)attr.filesize; + } + + // Add detail level info + if (result.level >= storageInfoLevelDetail) + { + if ((attr.flags & LIBSSH2_SFTP_ATTR_UIDGID) != 0) + { + result.groupId = (unsigned int)attr.gid; + result.userId = (unsigned int)attr.uid; + } + + if ((attr.flags & LIBSSH2_SFTP_ATTR_PERMISSIONS) != 0) + result.mode = attr.permissions & (LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG | LIBSSH2_SFTP_S_IRWXO); + + if (result.type == storageTypeLink) + { + char linkDestination[PATH_MAX] = {0}; + ssize_t linkDestinationSize = 0; + + Wait *const wait = waitNew(this->timeout); + + do + { + linkDestinationSize = libssh2_sftp_symlink_ex( + this->sftpSession, strZ(file), (unsigned int)strSize(file), linkDestination, PATH_MAX - 1, + LIBSSH2_SFTP_READLINK); + } + while (linkDestinationSize == LIBSSH2_ERROR_EAGAIN && waitMore(wait)); + + waitFree(wait); + + if (linkDestinationSize < 0) + THROW_FMT(FileReadError, "unable to get destination for link '%s'", strZ(file)); + + result.linkDestination = strNewZN(linkDestination, (size_t)linkDestinationSize); + } + } + } + + FUNCTION_LOG_RETURN(STORAGE_INFO, result); +} + +/**********************************************************************************************************************************/ +// Helper function to get info for a file if it exists. This logic can't live directly in storageSftpList() because there is a race +// condition where a file might exist while listing the directory but it is gone before stat() can be called. In order to get +// complete test coverage this function must be split out. +static void +storageSftpListEntry( + StorageSftp *const this, StorageList *const list, const String *const path, const char *const name, + const StorageInfoLevel level) +{ + FUNCTION_TEST_BEGIN(); + FUNCTION_TEST_PARAM(STORAGE_SFTP, this); + FUNCTION_TEST_PARAM(STORAGE_LIST, list); + FUNCTION_TEST_PARAM(STRING, path); + FUNCTION_TEST_PARAM(STRINGZ, name); + FUNCTION_TEST_PARAM(ENUM, level); + FUNCTION_TEST_END(); + + FUNCTION_AUDIT_HELPER(); + + ASSERT(this != NULL); + ASSERT(list != NULL); + ASSERT(path != NULL); + ASSERT(name != NULL); + + StorageInfo info = storageInterfaceInfoP(this, strNewFmt("%s/%s", strZ(path), name), level); + + if (info.exists) + { + info.name = STR(name); + storageLstAdd(list, &info); + } + + FUNCTION_TEST_RETURN_VOID(); +} + +static StorageList * +storageSftpList(THIS_VOID, const String *const path, const StorageInfoLevel level, const StorageInterfaceListParam param) +{ + THIS(StorageSftp); + + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(STORAGE_SFTP, this); + FUNCTION_LOG_PARAM(STRING, path); + FUNCTION_LOG_PARAM(ENUM, level); + (void)param; // No parameters are used + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + ASSERT(path != NULL); + + StorageList *result = NULL; + + // Open the directory for read + LIBSSH2_SFTP_HANDLE *sftpHandle; + Wait *const wait = waitNew(this->timeout); + + do + { + sftpHandle = libssh2_sftp_open_ex(this->sftpSession, strZ(path), (unsigned int)strSize(path), 0, 0, LIBSSH2_SFTP_OPENDIR); + } + while (sftpHandle == NULL && libssh2_session_last_errno(this->session) == LIBSSH2_ERROR_EAGAIN && waitMore(wait)); + + waitFree(wait); + + // If the directory could not be opened process errors and report missing directories + if (sftpHandle == NULL) + { + const int rc = libssh2_session_last_errno(this->session); + + // If sftpHandle == NULL is due to LIBSSH2_FX_NO_SUCH_FILE, do not throw error here, return NULL result + if (!storageSftpLibSsh2FxNoSuchFile(this, rc)) + { + storageSftpEvalLibSsh2Error( + rc, libssh2_sftp_last_error(this->sftpSession), &PathOpenError, strNewFmt(STORAGE_ERROR_LIST_INFO, strZ(path)), + NULL); + } + } + else + { + // Directory was found + result = storageLstNew(level); + + TRY_BEGIN() + { + MEM_CONTEXT_TEMP_RESET_BEGIN() + { + LIBSSH2_SFTP_ATTRIBUTES attr; + char filename[PATH_MAX] = {0}; + int len; + + Wait *wait = waitNew(this->timeout); + + // Read the directory entries + do + { + len = libssh2_sftp_readdir_ex(sftpHandle, filename, PATH_MAX - 1, NULL, 0, &attr); + + if (len > 0) + { + filename[len] = '\0'; + + // Always skip . and .. + if (!strEqZ(DOT_STR, filename) && !strEqZ(DOTDOT_STR, filename)) + { + if (level == storageInfoLevelExists) + { + const StorageInfo storageInfo = + { + .name = STR(filename), + .level = storageInfoLevelExists, + .exists = true, + }; + + storageLstAdd(result, &storageInfo); + } + else + storageSftpListEntry(this, result, path, filename, level); + } + + // Reset the memory context occasionally so we don't use too much memory or slow down processing + MEM_CONTEXT_TEMP_RESET(1000); + + // Reset the timeout so we don't timeout before reading all entries + waitFree(wait); + wait = waitNew(this->timeout); + } + } + while (len > 0 || (len == LIBSSH2_ERROR_EAGAIN && waitMore(wait))); + + waitFree(wait); + } + MEM_CONTEXT_TEMP_END(); + } + FINALLY() + { + int rc; + Wait *const wait = waitNew(this->timeout); + + do + { + rc = libssh2_sftp_closedir(sftpHandle); + } + while (rc == LIBSSH2_ERROR_EAGAIN && waitMore(wait)); + + waitFree(wait); + + if (rc != 0) + THROW_FMT(PathCloseError, "unable to close path '%s' after listing", strZ(path)); + + sftpHandle = NULL; + } + TRY_END(); + } + + FUNCTION_LOG_RETURN(STORAGE_LIST, result); +} + +/**********************************************************************************************************************************/ +static void +storageSftpRemove(THIS_VOID, const String *const file, const StorageInterfaceRemoveParam param) +{ + THIS(StorageSftp); + + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(STORAGE_SFTP, this); + FUNCTION_LOG_PARAM(STRING, file); + FUNCTION_LOG_PARAM(BOOL, param.errorOnMissing); + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + ASSERT(file != NULL); + + // Attempt to unlink the file + int rc; + Wait *const wait = waitNew(this->timeout); + + do + { + rc = libssh2_sftp_unlink_ex(this->sftpSession, strZ(file), (unsigned int)strSize(file)); + } + while (rc == LIBSSH2_ERROR_EAGAIN && waitMore(wait)); + + if (rc != 0) + { + if (rc == LIBSSH2_ERROR_SFTP_PROTOCOL) + { + if (param.errorOnMissing || !storageSftpLibSsh2FxNoSuchFile(this, rc)) + { + storageSftpEvalLibSsh2Error( + rc, libssh2_sftp_last_error(this->sftpSession), &FileRemoveError, + strNewFmt("unable to remove '%s'", strZ(file)), NULL); + } + } + else + { + if (param.errorOnMissing) + THROW_FMT(FileRemoveError, "unable to remove '%s'", strZ(file)); + } + } + + FUNCTION_LOG_RETURN_VOID(); +} + +/**********************************************************************************************************************************/ +static StorageRead * +storageSftpNewRead(THIS_VOID, const String *const file, const bool ignoreMissing, const StorageInterfaceNewReadParam param) +{ + THIS(StorageSftp); + + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(STORAGE_SFTP, this); + FUNCTION_LOG_PARAM(STRING, file); + FUNCTION_LOG_PARAM(BOOL, ignoreMissing); + FUNCTION_LOG_PARAM(UINT64, param.offset); + FUNCTION_LOG_PARAM(VARIANT, param.limit); + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + ASSERT(file != NULL); + + FUNCTION_LOG_RETURN( + STORAGE_READ, + storageReadSftpNew( + this, file, ignoreMissing, this->ioSession, this->session, this->sftpSession, this->sftpHandle, this->timeout, + param.offset, param.limit)); +} + +/**********************************************************************************************************************************/ +static StorageWrite * +storageSftpNewWrite(THIS_VOID, const String *const file, const StorageInterfaceNewWriteParam param) +{ + THIS(StorageSftp); + + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(STORAGE_SFTP, this); + FUNCTION_LOG_PARAM(STRING, file); + FUNCTION_LOG_PARAM(MODE, param.modeFile); + FUNCTION_LOG_PARAM(MODE, param.modePath); + FUNCTION_LOG_PARAM(STRING, param.user); + FUNCTION_LOG_PARAM(STRING, param.group); + FUNCTION_LOG_PARAM(TIME, param.timeModified); + FUNCTION_LOG_PARAM(BOOL, param.createPath); + FUNCTION_LOG_PARAM(BOOL, param.syncFile); + FUNCTION_LOG_PARAM(BOOL, param.syncPath); + FUNCTION_LOG_PARAM(BOOL, param.atomic); + FUNCTION_LOG_PARAM(BOOL, param.truncate); + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + ASSERT(file != NULL); + ASSERT(param.createPath); + ASSERT(param.truncate); + ASSERT(param.user == NULL); + ASSERT(param.group == NULL); + ASSERT(param.timeModified == 0); + + FUNCTION_LOG_RETURN( + STORAGE_WRITE, + storageWriteSftpNew( + this, file, this->ioSession, this->session, this->sftpSession, this->sftpHandle, this->timeout, param.modeFile, + param.modePath, param.user, param.group, param.timeModified, param.createPath, param.syncFile, + this->interface.pathSync != NULL ? param.syncPath : false, param.atomic, param.truncate)); +} + +/**********************************************************************************************************************************/ +static void +storageSftpPathCreate( + THIS_VOID, const String *const path, const bool errorOnExists, const bool noParentCreate, const mode_t mode, + const StorageInterfacePathCreateParam param) +{ + THIS(StorageSftp); + + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(STORAGE_SFTP, this); + FUNCTION_LOG_PARAM(STRING, path); + FUNCTION_LOG_PARAM(BOOL, errorOnExists); + FUNCTION_LOG_PARAM(BOOL, noParentCreate); + FUNCTION_LOG_PARAM(MODE, mode); + (void)param; // No parameters are used + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + ASSERT(path != NULL); + + int rc; + Wait *const wait = waitNew(this->timeout); + + // Attempt to create the directory + do + { + rc = libssh2_sftp_mkdir_ex(this->sftpSession, strZ(path), (unsigned int)strSize(path), (int)mode); + } + while (rc == LIBSSH2_ERROR_EAGAIN && waitMore(wait)); + + waitFree(wait); + + if (rc != 0) + { + if (rc == LIBSSH2_ERROR_SFTP_PROTOCOL) + { + uint64_t sftpErrno = libssh2_sftp_last_error(this->sftpSession); + + // libssh2 may return LIBSSH2_FX_FAILURE if the directory already exists + if (sftpErrno == LIBSSH2_FX_FAILURE) + { + // Check if the directory already exists + LIBSSH2_SFTP_ATTRIBUTES attr; + + Wait *const wait = waitNew(this->timeout); + + do + { + rc = libssh2_sftp_stat_ex( + this->sftpSession, strZ(path), (unsigned int)strSize(path), LIBSSH2_SFTP_STAT, &attr); + } + while (rc == LIBSSH2_ERROR_EAGAIN && waitMore(wait)); + + waitFree(wait); + + // If rc = 0 then already exists + if (rc == 0 && errorOnExists) + THROW_FMT(PathCreateError, "unable to create path '%s': path already exists", strZ(path)); + } + // If the parent path does not exist then create it if allowed + else if (sftpErrno == LIBSSH2_FX_NO_SUCH_FILE && !noParentCreate) + { + String *const pathParent = strPath(path); + + storageInterfacePathCreateP(this, pathParent, errorOnExists, noParentCreate, mode); + storageInterfacePathCreateP(this, path, errorOnExists, noParentCreate, mode); + + strFree(pathParent); + } + else if (sftpErrno != LIBSSH2_FX_FILE_ALREADY_EXISTS || errorOnExists) + THROW_FMT(PathCreateError, "sftp error unable to create path '%s'", strZ(path)); + } + else + THROW_FMT(PathCreateError, "ssh2 error [%d] unable to create path '%s'", rc, strZ(path)); + } + + FUNCTION_LOG_RETURN_VOID(); +} + +static bool +storageSftpPathRemove(THIS_VOID, const String *const path, const bool recurse, const StorageInterfacePathRemoveParam param) +{ + THIS(StorageSftp); + + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(STORAGE_SFTP, this); + FUNCTION_LOG_PARAM(STRING, path); + FUNCTION_LOG_PARAM(BOOL, recurse); + (void)param; // No parameters are used + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + ASSERT(path != NULL); + + bool result = true; + + MEM_CONTEXT_TEMP_BEGIN() + { + // Recurse if requested + if (recurse) + { + StorageList *const list = storageInterfaceListP(this, path, storageInfoLevelExists); + + if (list != NULL) + { + MEM_CONTEXT_TEMP_RESET_BEGIN() + { + for (unsigned int listIdx = 0; listIdx < storageLstSize(list); listIdx++) + { + const String *const file = strNewFmt("%s/%s", strZ(path), strZ(storageLstGet(list, listIdx).name)); + + // Rather than stat the file to discover what type it is, just try to unlink it and see what happens + int rc; + Wait *const wait = waitNew(this->timeout); + + do + { + rc = libssh2_sftp_unlink_ex(this->sftpSession, strZ(file), (unsigned int)strSize(file)); + } + while (rc == LIBSSH2_ERROR_EAGAIN && waitMore(wait)); + + waitFree(wait); + + if (rc != 0) + { + // Attempting to unlink a directory appears to return LIBSSH2_FX_FAILURE + if (rc == LIBSSH2_ERROR_SFTP_PROTOCOL && + libssh2_sftp_last_error(this->sftpSession) == LIBSSH2_FX_FAILURE) + { + storageInterfacePathRemoveP(this, file, true); + } + else + THROW_FMT(PathRemoveError, STORAGE_ERROR_PATH_REMOVE_FILE, strZ(file)); + } + + // Reset the memory context occasionally so we don't use too much memory or slow down processing + MEM_CONTEXT_TEMP_RESET(1000); + } + } + MEM_CONTEXT_TEMP_END(); + } + } + + // Delete the path + int rc; + Wait *const wait = waitNew(this->timeout); + + do + { + rc = libssh2_sftp_rmdir_ex(this->sftpSession, strZ(path), (unsigned int)strSize(path)); + } + while (rc == LIBSSH2_ERROR_EAGAIN && waitMore(wait)); + + waitFree(wait); + + if (rc != 0) + { + if (rc == LIBSSH2_ERROR_SFTP_PROTOCOL) + { + if (libssh2_sftp_last_error(this->sftpSession) != LIBSSH2_FX_NO_SUCH_FILE) + THROW_FMT(PathRemoveError, STORAGE_ERROR_PATH_REMOVE, strZ(path)); + + // Path does not exist + result = false; + } + else + { + // Path does not exist + result = false; + + THROW_FMT(PathRemoveError, STORAGE_ERROR_PATH_REMOVE, strZ(path)); + } + } + } + MEM_CONTEXT_TEMP_END(); + + FUNCTION_LOG_RETURN(BOOL, result); +} + +/**********************************************************************************************************************************/ +static const StorageInterface storageInterfaceSftp = +{ + .feature = 1 << storageFeaturePath | 1 << storageFeatureInfoDetail, + + .info = storageSftpInfo, + .list = storageSftpList, + .newRead = storageSftpNewRead, + .newWrite = storageSftpNewWrite, + .pathCreate = storageSftpPathCreate, + .pathRemove = storageSftpPathRemove, + .remove = storageSftpRemove, +}; + +FN_EXTERN Storage * +storageSftpNew( + const String *const path, const String *const host, const unsigned int port, const String *const user, + const TimeMSec timeout, const String *const keyPriv, const StringId hostKeyHashType, const StorageSftpNewParam param) +{ + FUNCTION_LOG_BEGIN(logLevelDebug); + FUNCTION_LOG_PARAM(STRING, path); + FUNCTION_LOG_PARAM(STRING, host); + FUNCTION_LOG_PARAM(UINT, port); + FUNCTION_LOG_PARAM(STRING, user); + FUNCTION_LOG_PARAM(TIME_MSEC, timeout); + FUNCTION_LOG_PARAM(STRING, keyPriv); + FUNCTION_LOG_PARAM(STRING_ID, hostKeyHashType); + FUNCTION_LOG_PARAM(STRING, param.keyPub); + FUNCTION_TEST_PARAM(STRING, param.keyPassphrase); + FUNCTION_LOG_PARAM(STRING, param.hostFingerprint); + FUNCTION_LOG_PARAM(MODE, param.modeFile); + FUNCTION_LOG_PARAM(MODE, param.modePath); + FUNCTION_LOG_PARAM(BOOL, param.write); + FUNCTION_LOG_PARAM(FUNCTIONP, param.pathExpressionFunction); + FUNCTION_LOG_END(); + + ASSERT(path != NULL); + ASSERT(host != NULL); + ASSERT(port != 0); + ASSERT(user != NULL); + ASSERT(keyPriv != NULL); + ASSERT(hostKeyHashType != 0); + + // Initialize user module + userInit(); + + // Create the object + OBJ_NEW_BEGIN(StorageSftp, .childQty = MEM_CONTEXT_QTY_MAX, .callbackQty = 1) + { + *this = (StorageSftp) + { + .interface = storageInterfaceSftp, + .timeout = timeout, + }; + + if (libssh2_init(0) != 0) + THROW_FMT(ServiceError, "unable to init libssh2"); + + this->ioSession = ioClientOpen(sckClientNew(host, port, timeout, timeout)); + + this->session = libssh2_session_init(); + if (this->session == NULL) + THROW_FMT(ServiceError, "unable to init libssh2 session"); + + // Returns void + libssh2_session_set_blocking(this->session, 0); + + int handshakeStatus = 0; + Wait *wait = waitNew(timeout); + + do + { + handshakeStatus = libssh2_session_handshake(this->session, ioSessionFd(this->ioSession)); + } + while (handshakeStatus == LIBSSH2_ERROR_EAGAIN && waitMore(wait)); + + waitFree(wait); + + if (handshakeStatus != 0) + THROW_FMT(ServiceError, "libssh2 handshake failed [%d]", handshakeStatus); + + int hashType = LIBSSH2_HOSTKEY_HASH_SHA1; + size_t hashSize = 0; + + // Verify that the fingerprint[N] buffer declared below is large enough when adding a new hashType + switch (hostKeyHashType) + { + case hashTypeMd5: + hashType = LIBSSH2_HOSTKEY_HASH_MD5; + hashSize = HASH_TYPE_M5_SIZE; + break; + + case hashTypeSha1: + hashType = LIBSSH2_HOSTKEY_HASH_SHA1; + hashSize = HASH_TYPE_SHA1_SIZE; + break; + +#ifdef LIBSSH2_HOSTKEY_HASH_SHA256 + case hashTypeSha256: + hashType = LIBSSH2_HOSTKEY_HASH_SHA256; + hashSize = HASH_TYPE_SHA256_SIZE; + break; +#endif // LIBSSH2_HOSTKEY_HASH_SHA256 + + default: + THROW_FMT( + ServiceError, "requested ssh2 hostkey hash type (%s) not available", strZ(strIdToStr(hostKeyHashType))); + break; + } + + const char *binaryFingerprint = libssh2_hostkey_hash(this->session, hashType); + + if (binaryFingerprint == NULL) + THROW_FMT(ServiceError, "libssh2 hostkey hash failed: libssh2 errno [%d]", libssh2_session_last_errno(this->session)); + + // Compare fingerprint if provided + if (param.hostFingerprint != NULL) + { + // 256 bytes is large enough to hold the hex representation of currently supported hash types. The hex encoded version + // requires twice as much space (hashSize * 2) as the raw version. + char fingerprint[256]; + + encodeToStr(encodingHex, (unsigned char *)binaryFingerprint, hashSize, fingerprint); + + if (strcmp(fingerprint, strZ(param.hostFingerprint)) != 0) + { + THROW_FMT( + ServiceError, "host [%s] and configured fingerprint (repo-sftp-host-fingerprint) [%s] do not match", + fingerprint, strZ(param.hostFingerprint)); + } + } + + LOG_DEBUG_FMT("attempting public key authentication"); + + int rc; + wait = waitNew(timeout); + + do + { + rc = libssh2_userauth_publickey_fromfile( + this->session, strZ(user), strZNull(param.keyPub), strZ(keyPriv), strZNull(param.keyPassphrase)); + } + while (rc == LIBSSH2_ERROR_EAGAIN && waitMore(wait)); + + waitFree(wait); + + if (rc != 0) + { + storageSftpEvalLibSsh2Error( + rc, libssh2_sftp_last_error(this->sftpSession), &ServiceError, + STRDEF("public key authentication failed"), + STRDEF( + "HINT: libssh2 compiled against non-openssl libraries requires --repo-sftp-private-key-file and" + " --repo-sftp-public-key-file to be provided\n" + "HINT: libssh2 versions before 1.9.0 expect a PEM format keypair, try ssh-keygen -m PEM -t rsa -P \"\" to" + " generate the keypair")); + } + + wait = waitNew(timeout); + + do + { + this->sftpSession = libssh2_sftp_init(this->session); + } + while (this->sftpSession == NULL && waitMore(wait)); + + waitFree(wait); + + if (this->sftpSession == NULL) + THROW_FMT(ServiceError, "unable to init libssh2_sftp session"); + + // Ensure libssh2/libssh2_sftp resources freed + memContextCallbackSet(objMemContext(this), storageSftpLibSsh2SessionFreeResource, this); + } + OBJ_NEW_END(); + + FUNCTION_LOG_RETURN( + STORAGE, + storageNew( + STORAGE_SFTP_TYPE, path, param.modeFile == 0 ? STORAGE_MODE_FILE_DEFAULT : param.modeFile, + param.modePath == 0 ? STORAGE_MODE_PATH_DEFAULT : param.modePath, param.write, param.pathExpressionFunction, + this, this->interface)); +} + +#endif // HAVE_LIBSSH2 diff --git a/src/storage/sftp/storage.h b/src/storage/sftp/storage.h new file mode 100644 index 000000000..0a5d10464 --- /dev/null +++ b/src/storage/sftp/storage.h @@ -0,0 +1,40 @@ +/*********************************************************************************************************************************** +SFTP Storage +***********************************************************************************************************************************/ +#ifndef STORAGE_SFTP_STORAGE_H +#define STORAGE_SFTP_STORAGE_H + +#include "storage/storage.h" + +/*********************************************************************************************************************************** +Storage type +***********************************************************************************************************************************/ +#define STORAGE_SFTP_TYPE STRID5("sftp", 0x850d30) + +#ifdef HAVE_LIBSSH2 + +/*********************************************************************************************************************************** +Constructors +***********************************************************************************************************************************/ +typedef struct StorageSftpNewParam +{ + VAR_PARAM_HEADER; + bool write; + mode_t modeFile; + mode_t modePath; + StoragePathExpressionCallback *pathExpressionFunction; + const String *keyPub; + const String *keyPassphrase; + const String *hostFingerprint; +} StorageSftpNewParam; + +#define storageSftpNewP(path, host, port, user, timeout, keyPriv, hostKeyHashType, ...) \ + storageSftpNew(path, host, port, user, timeout, keyPriv, hostKeyHashType, (StorageSftpNewParam){VAR_PARAM_INIT, __VA_ARGS__}) + +FN_EXTERN Storage *storageSftpNew( + const String *path, const String *host, unsigned int port, const String *user, TimeMSec timeout, const String *keyPriv, + StringId hostKeyHashType, const StorageSftpNewParam param); + +#endif // HAVE_LIBSSH2 + +#endif diff --git a/src/storage/sftp/storage.intern.h b/src/storage/sftp/storage.intern.h new file mode 100644 index 000000000..c8365ee8b --- /dev/null +++ b/src/storage/sftp/storage.intern.h @@ -0,0 +1,31 @@ +/*********************************************************************************************************************************** +SFTP Storage Internal +***********************************************************************************************************************************/ +#ifndef STORAGE_SFTP_STORAGE_INTERN_H +#define STORAGE_SFTP_STORAGE_INTERN_H + +#include +#include + +#include "storage/sftp/storage.h" + +/*********************************************************************************************************************************** +Object type +***********************************************************************************************************************************/ +typedef struct StorageSftp StorageSftp; + +/*********************************************************************************************************************************** +Functions +***********************************************************************************************************************************/ +FN_EXTERN void storageSftpEvalLibSsh2Error( + int ssh2Errno, uint64_t sftpErrno, const ErrorType *errorType, const String *msg, const String *hint); + +/*********************************************************************************************************************************** +Macros for function logging +***********************************************************************************************************************************/ +#define FUNCTION_LOG_STORAGE_SFTP_TYPE \ + StorageSftp * +#define FUNCTION_LOG_STORAGE_SFTP_FORMAT(value, buffer, bufferSize) \ + objNameToLog(value, "StorageSftp *", buffer, bufferSize) + +#endif diff --git a/src/storage/sftp/write.c b/src/storage/sftp/write.c new file mode 100644 index 000000000..3eb456477 --- /dev/null +++ b/src/storage/sftp/write.c @@ -0,0 +1,414 @@ +/*********************************************************************************************************************************** +SFTP Storage File Write +***********************************************************************************************************************************/ +#include "build.auto.h" + +#ifdef HAVE_LIBSSH2 + +#include "common/debug.h" +#include "common/log.h" +#include "common/user.h" +#include "common/wait.h" +#include "storage/sftp/write.h" +#include "storage/write.intern.h" + +/*********************************************************************************************************************************** +Object type +***********************************************************************************************************************************/ +typedef struct StorageWriteSftp +{ + StorageWriteInterface interface; // Interface + StorageSftp *storage; // Storage that created this object + + const String *nameTmp; // Temporary filename utilized for atomic ops + const String *path; // Utilized for path operations + IoSession *ioSession; // IoSession (socket) connection to SFTP server + LIBSSH2_SESSION *session; // LibSsh2 session + LIBSSH2_SFTP *sftpSession; // LibSsh2 session sftp session + LIBSSH2_SFTP_HANDLE *sftpHandle; // LibSsh2 session sftp handle + TimeMSec timeout; // Session timeout +} StorageWriteSftp; + +/*********************************************************************************************************************************** +Macros for function logging +***********************************************************************************************************************************/ +#define FUNCTION_LOG_STORAGE_WRITE_SFTP_TYPE \ + StorageWriteSftp * +#define FUNCTION_LOG_STORAGE_WRITE_SFTP_FORMAT(value, buffer, bufferSize) \ + objNameToLog(value, "StorageWriteSftp", buffer, bufferSize) + +/*********************************************************************************************************************************** +Open the file +***********************************************************************************************************************************/ +static void +storageWriteSftpOpen(THIS_VOID) +{ + THIS(StorageWriteSftp); + + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(STORAGE_WRITE_SFTP, this); + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + ASSERT(this->sftpSession != NULL); + + const unsigned long int flags = LIBSSH2_FXF_CREAT | LIBSSH2_FXF_WRITE | LIBSSH2_FXF_TRUNC; + + // Open the file + Wait *const wait = waitNew(this->timeout); + + do + { + this->sftpHandle = libssh2_sftp_open_ex( + this->sftpSession, strZ(this->nameTmp), (unsigned int)strSize(this->nameTmp), flags, (int)this->interface.modeFile, + LIBSSH2_SFTP_OPENFILE); + } + while (this->sftpHandle == NULL && libssh2_session_last_errno(this->session) == LIBSSH2_ERROR_EAGAIN && waitMore(wait)); + + waitFree(wait); + + // Attempt to create the path if it is missing + if (this->sftpHandle == NULL && libssh2_session_last_errno(this->session) == LIBSSH2_ERROR_SFTP_PROTOCOL && + libssh2_sftp_last_error(this->sftpSession) == LIBSSH2_FX_NO_SUCH_FILE) + { + // Create the path + storageInterfacePathCreateP(this->storage, this->path, false, false, this->interface.modePath); + + // Open file again + Wait *const wait = waitNew(this->timeout); + + do + { + this->sftpHandle = libssh2_sftp_open_ex( + this->sftpSession, strZ(this->nameTmp), (unsigned int)strSize(this->nameTmp), flags, (int)this->interface.modeFile, + LIBSSH2_SFTP_OPENFILE); + } + while (this->sftpHandle == NULL && (libssh2_session_last_errno(this->session) == LIBSSH2_ERROR_EAGAIN && waitMore(wait))); + + waitFree(wait); + } + + // Handle error + if (this->sftpHandle == NULL) + { + const int rc = libssh2_session_last_errno(this->session); + + if (rc == LIBSSH2_ERROR_SFTP_PROTOCOL) + { + uint64_t sftpErr = libssh2_sftp_last_error(this->sftpSession); + + if (sftpErr == LIBSSH2_FX_NO_SUCH_FILE) + THROW_FMT(FileMissingError, STORAGE_ERROR_WRITE_MISSING, strZ(this->interface.name)); + else + { + storageSftpEvalLibSsh2Error( + rc, sftpErr, &FileOpenError, strNewFmt(STORAGE_ERROR_WRITE_OPEN, strZ(this->interface.name)), NULL); + } + } + else + THROW_FMT(FileOpenError, STORAGE_ERROR_WRITE_OPEN, strZ(this->interface.name)); + } + + FUNCTION_LOG_RETURN_VOID(); +} + +/*********************************************************************************************************************************** +Write to the file +***********************************************************************************************************************************/ +static void +storageWriteSftp(THIS_VOID, const Buffer *const buffer) +{ + THIS(StorageWriteSftp); + + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(STORAGE_WRITE_SFTP, this); + FUNCTION_LOG_PARAM(BUFFER, buffer); + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + ASSERT(buffer != NULL); + ASSERT(this->sftpHandle != NULL); + + ssize_t rc; + size_t remains = bufUsed(buffer); // Amount left to write + size_t offset = 0; // Offset into the buffer + + // Loop until all the data is written + do + { + Wait *const wait = waitNew(this->timeout); + + do + { + rc = libssh2_sftp_write(this->sftpHandle, (const char *)bufPtrConst(buffer) + offset, remains); + } + while (rc == LIBSSH2_ERROR_EAGAIN && waitMore(wait)); + + waitFree(wait); + + // Break on error. Error will be thrown below the loop. + if (rc < 0) + break; + + // Offset for next write start point + offset += (size_t)rc; + + // Update amount left to write + remains -= (size_t)rc; + } + while (remains); + + if (rc < 0) + THROW_FMT(FileWriteError, "unable to write '%s'", strZ(this->nameTmp)); + + FUNCTION_LOG_RETURN_VOID(); +} + +/*********************************************************************************************************************************** +Unlink already existing file +***********************************************************************************************************************************/ +static void +storageWriteSftpUnlinkExisting(THIS_VOID) +{ + THIS(StorageWriteSftp); + + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(STORAGE_WRITE_SFTP, this); + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + + int rc; + Wait *const wait = waitNew(this->timeout); + + do + { + rc = libssh2_sftp_unlink_ex(this->sftpSession, strZ(this->interface.name), (unsigned int)strSize(this->interface.name)); + } + while (rc == LIBSSH2_ERROR_EAGAIN && waitMore(wait)); + + waitFree(wait); + + if (rc) + { + storageSftpEvalLibSsh2Error( + rc, libssh2_sftp_last_error(this->sftpSession), &FileRemoveError, + strNewFmt("unable to remove existing '%s'", strZ(this->interface.name)), NULL); + } + + FUNCTION_LOG_RETURN_VOID(); +} + +/*********************************************************************************************************************************** +Rename a file +***********************************************************************************************************************************/ +static void +storageWriteSftpRename(THIS_VOID) +{ + THIS(StorageWriteSftp); + + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(STORAGE_WRITE_SFTP, this); + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + + int rc; + Wait *const wait = waitNew(this->timeout); + + do + { + rc = libssh2_sftp_rename_ex( + this->sftpSession, strZ(this->nameTmp), (unsigned int)strSize(this->nameTmp), strZ(this->interface.name), + (unsigned int)strSize(this->interface.name), + LIBSSH2_SFTP_RENAME_OVERWRITE | LIBSSH2_SFTP_RENAME_ATOMIC | LIBSSH2_SFTP_RENAME_NATIVE); + } + while (rc == LIBSSH2_ERROR_EAGAIN && waitMore(wait)); + + waitFree(wait); + + if (rc) + { + storageSftpEvalLibSsh2Error( + rc, libssh2_sftp_last_error(this->sftpSession), &FileRemoveError, + strNewFmt("unable to move '%s' to '%s'", strZ(this->nameTmp), strZ(this->interface.name)), NULL); + } + + FUNCTION_LOG_RETURN_VOID(); +} + +/*********************************************************************************************************************************** +Close the file +***********************************************************************************************************************************/ +static void +storageWriteSftpClose(THIS_VOID) +{ + THIS(StorageWriteSftp); + + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(STORAGE_WRITE_SFTP, this); + FUNCTION_LOG_END(); + + ASSERT(this != NULL); + + // Close if the file has not already been closed + if (this->sftpHandle != NULL) + { + int rc; + char *libSsh2ErrMsg; + int errMsgLen; + int libSsh2ErrNo; + + if (this->interface.syncFile) + { + Wait *const wait = waitNew(this->timeout); + + do + { + rc = libssh2_sftp_fsync(this->sftpHandle); + } + while (rc == LIBSSH2_ERROR_EAGAIN && waitMore(wait)); + + waitFree(wait); + + if (rc) + THROW_FMT(FileSyncError, STORAGE_ERROR_WRITE_SYNC, strZ(this->nameTmp)); + } + + // Close the file + Wait *const wait = waitNew(this->timeout); + + do + { + rc = libssh2_sftp_close(this->sftpHandle); + } + while (rc == LIBSSH2_ERROR_EAGAIN && waitMore(wait)); + + waitFree(wait); + + if (rc != 0) + { + libSsh2ErrNo = libssh2_session_last_error(this->session, &libSsh2ErrMsg, &errMsgLen, 0); + + THROW_FMT( + FileCloseError, + STORAGE_ERROR_WRITE_CLOSE ": libssh2 error [%d] %s", strZ(this->nameTmp), libSsh2ErrNo, libSsh2ErrMsg); + } + + this->sftpHandle = NULL; + + // Rename from temp file + if (this->interface.atomic) + { + Wait *const wait = waitNew(this->timeout); + + do + { + rc = libssh2_sftp_rename_ex( + this->sftpSession, strZ(this->nameTmp), (unsigned int)strSize(this->nameTmp), strZ(this->interface.name), + (unsigned int)strSize(this->interface.name), + LIBSSH2_SFTP_RENAME_OVERWRITE | LIBSSH2_SFTP_RENAME_ATOMIC | LIBSSH2_SFTP_RENAME_NATIVE); + } + while (rc == LIBSSH2_ERROR_EAGAIN && waitMore(wait)); + + waitFree(wait); + + if (rc) + { + // Some/most sftp servers will not rename over an existing file, in testing this returned LIBSSH2_FX_FAILURE + if (rc == LIBSSH2_ERROR_SFTP_PROTOCOL && libssh2_sftp_last_error(this->sftpSession) == LIBSSH2_FX_FAILURE) + { + // Remove the existing file and retry the rename + storageWriteSftpUnlinkExisting(this); + storageWriteSftpRename(this); + } + else + { + storageSftpEvalLibSsh2Error( + rc, libssh2_sftp_last_error(this->sftpSession), &FileCloseError, + strNewFmt("unable to move '%s' to '%s'", strZ(this->nameTmp), strZ(this->interface.name)), NULL); + } + } + } + } + + FUNCTION_LOG_RETURN_VOID(); +} + +/**********************************************************************************************************************************/ +FN_EXTERN StorageWrite * +storageWriteSftpNew( + StorageSftp *const storage, const String *const name, IoSession *const ioSession, LIBSSH2_SESSION *const session, + LIBSSH2_SFTP *const sftpSession, LIBSSH2_SFTP_HANDLE *const sftpHandle, const TimeMSec timeout, const mode_t modeFile, + const mode_t modePath, const String *const user, const String *const group, const time_t timeModified, const bool createPath, + const bool syncFile, const bool syncPath, const bool atomic, const bool truncate) +{ + FUNCTION_LOG_BEGIN(logLevelTrace); + FUNCTION_LOG_PARAM(STORAGE_SFTP, storage); + FUNCTION_LOG_PARAM(STRING, name); + FUNCTION_LOG_PARAM_P(VOID, session); + FUNCTION_LOG_PARAM_P(VOID, sftpSession); + FUNCTION_LOG_PARAM_P(VOID, sftpHandle); + FUNCTION_LOG_PARAM(TIME_MSEC, timeout); + FUNCTION_LOG_PARAM(MODE, modeFile); + FUNCTION_LOG_PARAM(MODE, modePath); + FUNCTION_LOG_PARAM(STRING, user); + FUNCTION_LOG_PARAM(STRING, group); + FUNCTION_LOG_PARAM(TIME, timeModified); + FUNCTION_LOG_PARAM(BOOL, createPath); + FUNCTION_LOG_PARAM(BOOL, syncFile); + FUNCTION_LOG_PARAM(BOOL, syncPath); + FUNCTION_LOG_PARAM(BOOL, atomic); + FUNCTION_LOG_PARAM(BOOL, truncate); + FUNCTION_LOG_END(); + + ASSERT(storage != NULL); + ASSERT(name != NULL); + ASSERT(modeFile != 0); + ASSERT(modePath != 0); + + OBJ_NEW_BEGIN(StorageWriteSftp, .childQty = MEM_CONTEXT_QTY_MAX) + { + *this = (StorageWriteSftp) + { + .storage = storage, + .path = strPath(name), + .ioSession = ioSession, + .session = session, + .sftpSession = sftpSession, + .sftpHandle = sftpHandle, + .timeout = timeout, + + .interface = (StorageWriteInterface) + { + .type = STORAGE_SFTP_TYPE, + .name = strDup(name), + .atomic = atomic, + .createPath = createPath, + .group = strDup(group), + .modeFile = modeFile, + .modePath = modePath, + .syncFile = syncFile, + .syncPath = syncPath, + .truncate = truncate, + .user = strDup(user), + .timeModified = timeModified, + + .ioInterface = (IoWriteInterface) + { + .close = storageWriteSftpClose, + .open = storageWriteSftpOpen, + .write = storageWriteSftp, + }, + }, + }; + + // Create temp file name + this->nameTmp = atomic ? strNewFmt("%s." STORAGE_FILE_TEMP_EXT, strZ(name)) : this->interface.name; + } + OBJ_NEW_END(); + + FUNCTION_LOG_RETURN(STORAGE_WRITE, storageWriteNew(this, &this->interface)); +} + +#endif // HAVE_LIBSSH2 diff --git a/src/storage/sftp/write.h b/src/storage/sftp/write.h new file mode 100644 index 000000000..ecd75c210 --- /dev/null +++ b/src/storage/sftp/write.h @@ -0,0 +1,19 @@ +/*********************************************************************************************************************************** +SFTP Storage File Write +***********************************************************************************************************************************/ +#ifndef STORAGE_SFTP_WRITE_H +#define STORAGE_SFTP_WRITE_H + +#include "common/io/session.h" +#include "storage/sftp/storage.h" +#include "storage/sftp/storage.intern.h" + +/*********************************************************************************************************************************** +Constructors +***********************************************************************************************************************************/ +FN_EXTERN StorageWrite *storageWriteSftpNew( + StorageSftp *storage, const String *name, IoSession *ioSession, LIBSSH2_SESSION *session, LIBSSH2_SFTP *sftpSession, + LIBSSH2_SFTP_HANDLE *sftpHandle, TimeMSec timeout, mode_t modeFile, mode_t modePath, const String *user, const String *group, + time_t timeModified, bool createPath, bool syncFile, bool syncPath, bool atomic, bool truncate); + +#endif diff --git a/test/Dockerfile b/test/Dockerfile index 2ba28dbbb..e88871a78 100644 --- a/test/Dockerfile +++ b/test/Dockerfile @@ -13,7 +13,7 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \ libdbd-pg-perl libxml-checker-perl libyaml-perl \ devscripts build-essential lintian git cloc txt2man debhelper libssl-dev zlib1g-dev libperl-dev libxml2-dev liblz4-dev \ liblz4-tool libpq-dev lcov autoconf-archive zstd libzstd-dev bzip2 libbz2-dev pkg-config libyaml-dev libc6-dbg wget meson \ - ccache valgrind tzdata uncrustify + ccache valgrind tzdata uncrustify libssh2-1-dev # Install Docker RUN groupadd -g5000 docker diff --git a/test/Vagrantfile b/test/Vagrantfile index 5cd81af2f..a5568426a 100644 --- a/test/Vagrantfile +++ b/test/Vagrantfile @@ -76,7 +76,7 @@ Vagrant.configure(2) do |config| echo 'Install Build Tools' && date apt-get install -y devscripts build-essential lintian git cloc txt2man debhelper libssl-dev zlib1g-dev libperl-dev \ libxml2-dev liblz4-dev liblz4-tool libpq-dev lcov autoconf-archive zstd libzstd-dev bzip2 libbz2-dev pkg-config \ - libyaml-dev libc6-dbg valgrind meson ccache uncrustify + libyaml-dev libc6-dbg valgrind meson ccache uncrustify libssh2-1-dev #----------------------------------------------------------------------------------------------------------------------- echo 'Install Docker' && date diff --git a/test/certificate/ssh/id_rsa b/test/certificate/ssh/id_rsa new file mode 100644 index 000000000..7bbfbe139 --- /dev/null +++ b/test/certificate/ssh/id_rsa @@ -0,0 +1,16 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXwIBAAKBgQDR0yJsZW5d5LcqteiOtv8d+FFeFFHDPI0VTcTOdMn1iDiIP1ou +X3Q2OyNjsBaDbsRJd+sp9IRq1LKX3zsBcgGZANwm0zduuNEPEU94ajS/uRoejIqY +/XkKOpnEF6ZbQ2S7TaE4sWeGLvba7kUFs0QTOO+N+nV2dMbdqZf6C8lazwIDAQAB +AoGBAJXa6xzrnFVmwgK5BKzYuX/YF5TPgk2j80ch0ct50buQXH/Cb0/rUH5i4jWS +T6Hy/DFUehnuzpvV6O9auTOhDs3BhEKFRuRLn1nBwTtZny5Hh+cw7azUCEHFCJlz +makCrVbgawtno6oU/pFgQm1FcxD0f+Me5ruNcLHqUZsPQwkRAkEA+8pG+ckOlz6R +AJLIHedmfcrEY9T7sfdo83bzMOz8H5soUUP4aOTLJYCla1LO7JdDnXMGo0KxaHBP +l8j5zDmVewJBANVVPDJr1w37m0FBi37QgUOAijVfLXgyPMxYp2uc9ddjncif0063 +0Wc0FQefoPszf3CDrHv/RHvhHq97jXDwTb0CQQDgH83NygoS1r57pCw9chzpG/R0 +aMEiSPhCvz757fj+qT3aGIal2AJ7/2c/gRZvwrWNETZ3XIZOUKqIkXzJLPjBAkEA +wnP799W2Y8d4/+VX2pMBkF7lG7sSviHEq1sP2BZtPBRQKSQNvw3scM7XcGh/mxmY +yx0qpqfKa8SKbNgI1+4iXQJBAOlg8MJLwkUtrG+p8wf69oCuZsnyv0K6UMDxm6/8 +cbvfmvODulYFaIahaqHWEZoRo5CLYZ7gN43WHPOrKxdDL78= +-----END RSA PRIVATE KEY----- + diff --git a/test/certificate/ssh/id_rsa.pub b/test/certificate/ssh/id_rsa.pub new file mode 100644 index 000000000..44808b51b --- /dev/null +++ b/test/certificate/ssh/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDR0yJsZW5d5LcqteiOtv8d+FFeFFHDPI0VTcTOdMn1iDiIP1ouX3Q2OyNjsBaDbsRJd+sp9IRq1LKX3zsBcgGZANwm0zduuNEPEU94ajS/uRoejIqY/XkKOpnEF6ZbQ2S7TaE4sWeGLvba7kUFs0QTOO+N+nV2dMbdqZf6C8lazw== diff --git a/test/ci.pl b/test/ci.pl index 4a44fb1cf..f30ed6a70 100755 --- a/test/ci.pl +++ b/test/ci.pl @@ -189,7 +189,7 @@ eval # Build list of packages that need to be installed my $strPackage = "make gcc ccache meson python3-pip git rsync zlib1g-dev libssl-dev libxml2-dev libpq-dev libyaml-dev pkg-config" . - " uncrustify"; + " uncrustify libssh2-1-dev"; # Add lcov when testing coverage if (vmCoverageC($strVm)) diff --git a/test/code-count/file-type.yaml b/test/code-count/file-type.yaml index abdf8930b..7a04ab1dd 100644 --- a/test/code-count/file-type.yaml +++ b/test/code-count/file-type.yaml @@ -1859,6 +1859,42 @@ src/storage/s3/write.h: class: core type: c/h +src/storage/sftp/helper.c: + class: core + type: c + +src/storage/sftp/helper.h: + class: core + type: c/h + +src/storage/sftp/read.c: + class: core + type: c + +src/storage/sftp/read.h: + class: core + type: c/h + +src/storage/sftp/storage.c: + class: core + type: c + +src/storage/sftp/storage.h: + class: core + type: c/h + +src/storage/sftp/storage.intern.h: + class: core + type: c/h + +src/storage/sftp/write.c: + class: core + type: c + +src/storage/sftp/write.h: + class: core + type: c/h + src/storage/storage.c: class: core type: c @@ -2163,6 +2199,22 @@ test/src/common/harnessInfo.h: class: test/harness type: c/h +test/src/common/harnessIo.c: + class: test/harness + type: c + +test/src/common/harnessIo.h: + class: test/harness + type: c/h + +test/src/common/harnessLibssh2.c: + class: test/harness + type: c + +test/src/common/harnessLibssh2.h: + class: test/harness + type: c/h + test/src/common/harnessLog.c: class: test/harness type: c @@ -2639,6 +2691,10 @@ test/src/module/storage/s3Test.c: class: test/module type: c +test/src/module/storage/sftpTest.c: + class: test/module + type: c + test/src/module/test/testTest.c: class: test/module type: c diff --git a/test/container.yaml b/test/container.yaml index 2e0e46085..429965c36 100644 --- a/test/container.yaml +++ b/test/container.yaml @@ -12,19 +12,9 @@ # - docker login -u pgbackrest # - VM=XXX;DATE=YYYYMMDDX;BASE=pgbackrest/test:${VM?}-base;docker tag ${BASE?} ${BASE?}-${DATE?} && docker push ${BASE?}-${DATE?} # ********************************************************************************************************************************** -20230427A: +20230513A: x86_64: - u22: 6a07d63ae869c3d84d1bf6622826df9fe768691d - -20230405A: - x86_64: - d10: 01a943f8d68ee5bbb2a5ee10cc474925c6cc0cde - -20221220A: - x86_64: - u20: 2db467d873c0aff06335592c8a22b8441b5c6440 - -20220726A: - x86_64: - f36: 099b329ca7988b05f2cb8ef759e146ea9faab108 - rh7: 6072f05804b369681efad5cebe01704cb9d2a81a + d10: 633def5323eeed51d7aa187ad1d61e854c47d6fa + f36: 640d3ed0d9786ef61263e27a77686a331f72c58e + rh7: 18dd0cbe19aa66dd9d72d312da5cb8c4bddea8b9 + u22: eba71d24c8636c7dfea5942f78a82a4566bef784 diff --git a/test/define.yaml b/test/define.yaml index e4a56532d..e362c9170 100644 --- a/test/define.yaml +++ b/test/define.yaml @@ -365,6 +365,12 @@ unit: total: 5 feature: SOCKET harness: server + harness: + name: socket + shim: + common/io/socket/client: + function: + - sckClientOpen coverage: - common/io/client @@ -598,6 +604,23 @@ unit: - storage/storage - storage/write + # ---------------------------------------------------------------------------------------------------------------------------- + - name: sftp + total: 20 + harness: libSsh2 + + coverage: + - storage/sftp/helper + - storage/sftp/read + - storage/sftp/storage + - storage/sftp/write + + include: + - storage/helper + - storage/read + - storage/storage + - storage/write + # ******************************************************************************************************************************** - name: postgres diff --git a/test/lib/pgBackRestTest/Common/ContainerTest.pm b/test/lib/pgBackRestTest/Common/ContainerTest.pm index 8f4913b02..2bb07f262 100644 --- a/test/lib/pgBackRestTest/Common/ContainerTest.pm +++ b/test/lib/pgBackRestTest/Common/ContainerTest.pm @@ -215,6 +215,7 @@ sub sshSetup } $strScript .= + " cp ${strUserPath}/.ssh/authorized_keys ${strUserPath}/.ssh/id_rsa.pub && \\\n" . " chown -R ${strUser}:${strGroup} ${strUserPath}/.ssh && \\\n" . " chmod 700 ${strUserPath}/.ssh && \\\n" . " chmod 600 ${strUserPath}/.ssh/*"; @@ -385,7 +386,8 @@ sub containerBuild " yum -y install openssh-server openssh-clients wget sudo valgrind git \\\n" . " perl perl-Digest-SHA perl-DBD-Pg perl-YAML-LibYAML openssl \\\n" . " gcc make perl-ExtUtils-MakeMaker perl-Test-Simple openssl-devel perl-ExtUtils-Embed rpm-build \\\n" . - " libyaml-devel zlib-devel libxml2-devel lz4-devel lz4 bzip2-devel bzip2 perl-JSON-PP ccache meson"; + " libyaml-devel zlib-devel libxml2-devel lz4-devel lz4 bzip2-devel bzip2 perl-JSON-PP ccache meson \\\n" . + " libssh2-devel"; } else { @@ -396,7 +398,8 @@ sub containerBuild " libdbd-pg-perl libhtml-parser-perl libssl-dev libperl-dev \\\n" . " libyaml-libyaml-perl tzdata devscripts lintian libxml-checker-perl txt2man debhelper \\\n" . " libppi-html-perl libtemplate-perl libtest-differences-perl zlib1g-dev libxml2-dev pkg-config \\\n" . - " libbz2-dev bzip2 libyaml-dev libjson-pp-perl liblz4-dev liblz4-tool gnupg lsb-release ccache meson"; + " libbz2-dev bzip2 libyaml-dev libjson-pp-perl liblz4-dev liblz4-tool gnupg lsb-release ccache meson \\\n" . + " libssh2-1-dev"; # This package is required to build valgrind on 32-bit if ($oVm->{$strOS}{&VM_ARCH} eq VM_ARCH_I386) @@ -612,6 +615,14 @@ sub containerBuild " echo '***********************************************' >> /etc/issue.net && \\\n" . " echo 'Banner /etc/issue.net' >> /etc/ssh/sshd_config"; + if ($strOS eq VM_U22) + { + $strScript .= sectionHeader() . + " echo '# Add PubkeyAcceptedAlgorithms (required for SFTP)' >> /etc/ssh/sshd_config && \\\n" . + " echo 'HostKeyAlgorithms=+ssh-rsa,ssh-rsa-cert-v01\@openssh.com' >> /etc/ssh/sshd_config && \\\n" . + " echo 'PubkeyAcceptedAlgorithms=+ssh-rsa,ssh-rsa-cert-v01\@openssh.com' >> /etc/ssh/sshd_config"; + } + $strScript .= sectionHeader() . "# Create test user\n" . ' ' . groupCreate($strOS, TEST_GROUP, TEST_GROUP_ID) . " && \\\n" . diff --git a/test/lib/pgBackRestTest/Common/VmTest.pm b/test/lib/pgBackRestTest/Common/VmTest.pm index 221bfc621..6f25f84fd 100644 --- a/test/lib/pgBackRestTest/Common/VmTest.pm +++ b/test/lib/pgBackRestTest/Common/VmTest.pm @@ -162,16 +162,15 @@ my $oyVm = &VM_DB => [ - PG_VERSION_10, PG_VERSION_11, PG_VERSION_12, PG_VERSION_13, PG_VERSION_14, + PG_VERSION_15, ], &VM_DB_TEST => [ - PG_VERSION_10, PG_VERSION_11, PG_VERSION_12, PG_VERSION_13, @@ -193,7 +192,6 @@ my $oyVm = &VM_DB => [ - PG_VERSION_10, PG_VERSION_11, PG_VERSION_12, PG_VERSION_13, @@ -269,6 +267,7 @@ my $oyVm = [ PG_VERSION_95, PG_VERSION_96, + PG_VERSION_10, PG_VERSION_15, PG_VERSION_16, ], diff --git a/test/lib/pgBackRestTest/Env/Host/HostBackupTest.pm b/test/lib/pgBackRestTest/Env/Host/HostBackupTest.pm index e1b5ce4eb..c1bef5f75 100644 --- a/test/lib/pgBackRestTest/Env/Host/HostBackupTest.pm +++ b/test/lib/pgBackRestTest/Env/Host/HostBackupTest.pm @@ -33,6 +33,7 @@ use pgBackRestTest::Env::Host::HostAzureTest; use pgBackRestTest::Env::Host::HostGcsTest; use pgBackRestTest::Env::Host::HostBaseTest; use pgBackRestTest::Env::Host::HostS3Test; +use pgBackRestTest::Env::Host::HostSftpTest; use pgBackRestTest::Env::Manifest; use pgBackRestTest::Common::ContainerTest; use pgBackRestTest::Common::ExecuteTest; @@ -92,6 +93,8 @@ use constant POSIX => STORAGE_P push @EXPORT, qw(POSIX); use constant S3 => 's3'; push @EXPORT, qw(S3); +use constant SFTP => 'sftp'; + push @EXPORT, qw(SFTP); use constant CFGOPTVAL_RESTORE_TYPE_DEFAULT => 'default'; push @EXPORT, qw(CFGOPTVAL_RESTORE_TYPE_DEFAULT); @@ -161,7 +164,7 @@ sub new bless $self, $class; # If repo is on local filesystem then set the repo-path locally - if ($oParam->{bRepoLocal}) + if ($oParam->{bRepoLocal} || $oParam->{strBackupDestination} eq HOST_SFTP) { $self->{strRepoPath} = $self->testRunGet()->testPath() . "/$$oParam{strBackupDestination}/" . HOST_PATH_REPO; } @@ -1265,6 +1268,35 @@ sub configCreate } } } + elsif ($oParam->{strStorage} eq SFTP) + { + my $oHostDb1 = $oHostDbPrimary; + my $oHostDb2 = $oHostDbStandby; + + if ($self->nameTest(HOST_DB_STANDBY)) + { + $oHostDb1 = $oHostDbStandby; + $oHostDb2 = $oHostDbPrimary; + } + + # Set a flag so we know there's a bogus host + $self->{bBogusHost} = true; + + # Set a valid replica to a higher index to ensure skipping indexes does not make a difference + $oParamHash{$strStanza}{"pg256-host"} = $oHostDb2->nameGet(); + $oParamHash{$strStanza}{"pg256-host-user"} = $oHostDb2->userGet(); + $oParamHash{$strStanza}{"pg256-host-cmd"} = $oHostDb2->backrestExe(); + $oParamHash{$strStanza}{"pg256-host-config"} = $oHostDb2->backrestConfig(); + $oParamHash{$strStanza}{"pg256-path"} = $oHostDb2->dbBasePath(); + + + # !!! is this needed??? + # Only test explicit ports on the backup server. This is so locally configured ports are also tested. + if (!$self->synthetic()) + { + $oParamHash{$strStanza}{"pg256-port"} = $oHostDb2->pgPort(); + } + } # If this is a database host if ($self->isHostDb()) @@ -1287,10 +1319,29 @@ sub configCreate # If the backup host is remote if (!$self->isHostBackup()) { - $oParamHash{&CFGDEF_SECTION_GLOBAL}{'repo1-host'} = $oHostBackup->nameGet(); $oParamHash{&CFGDEF_SECTION_GLOBAL}{'repo1-host-user'} = $oHostBackup->userGet(); - $oParamHash{&CFGDEF_SECTION_GLOBAL}{'repo1-host-cmd'} = $oHostBackup->backrestExe(); - $oParamHash{&CFGDEF_SECTION_GLOBAL}{'repo1-host-config'} = $oHostBackup->backrestConfig(); + + if ($oHostBackup->nameGet() eq HOST_SFTP) + { + $oParamHash{&CFGDEF_SECTION_GLOBAL}{'repo1-type'} = "sftp"; + $oParamHash{&CFGDEF_SECTION_GLOBAL}{'repo1-sftp-host'} = HOST_SFTP; + $oParamHash{&CFGDEF_SECTION_GLOBAL}{'repo1-sftp-host-key-hash-type'} = "sha1"; + $oParamHash{&CFGDEF_SECTION_GLOBAL}{'repo1-sftp-host-user'} = TEST_USER; + $oParamHash{&CFGDEF_SECTION_GLOBAL}{'repo1-sftp-private-key-file'} = testRunGet()->basePath() . SSH_PRIVATE_KEY; + $oParamHash{&CFGDEF_SECTION_GLOBAL}{'repo1-sftp-public-key-file'} = testRunGet()->basePath() . SSH_PUBLIC_KEY; + $oParamHash{&CFGDEF_SECTION_GLOBAL}{'repo1-path'} = $self->repoPath(); + + # At what count do we hit diminishing returns + $oParamHash{&CFGDEF_SECTION_GLOBAL}{'process-max'} = 8; + + $oParamHash{&CFGDEF_SECTION_GLOBAL . ':backup'}{'start-fast'} = 'y'; + } + else + { + $oParamHash{&CFGDEF_SECTION_GLOBAL}{'repo1-host'} = $oHostBackup->nameGet(); + $oParamHash{&CFGDEF_SECTION_GLOBAL}{'repo1-host-cmd'} = $oHostBackup->backrestExe(); + $oParamHash{&CFGDEF_SECTION_GLOBAL}{'repo1-host-config'} = $oHostBackup->backrestConfig(); + } if ($oParam->{bTls}) { diff --git a/test/lib/pgBackRestTest/Env/Host/HostBaseTest.pm b/test/lib/pgBackRestTest/Env/Host/HostBaseTest.pm index 7c5d0dd21..229055bbe 100644 --- a/test/lib/pgBackRestTest/Env/Host/HostBaseTest.pm +++ b/test/lib/pgBackRestTest/Env/Host/HostBaseTest.pm @@ -43,6 +43,8 @@ use constant HOST_AZURE => 'azure'; push @EXPORT, qw(HOST_AZURE); use constant HOST_S3 => 's3-server'; push @EXPORT, qw(HOST_S3); +use constant HOST_SFTP => 'sftp-srvr'; + push @EXPORT, qw(HOST_SFTP); #################################################################################################################################### # CA/cert/key constants @@ -59,6 +61,16 @@ use constant HOST_SERVER_CA => HOST_CERT use constant HOST_SERVER_CERT => HOST_CERT_PATH . 'pgbackrest-test-server.crt'; use constant HOST_SERVER_KEY => HOST_CERT_PATH . 'pgbackrest-test-server.key'; +#################################################################################################################################### +# SFTP key constants +#################################################################################################################################### +use constant SSH_KEY_PATH => '/test/certificate/ssh/'; + +use constant SSH_PRIVATE_KEY => SSH_KEY_PATH . 'id_rsa'; + push @EXPORT, qw(SSH_PRIVATE_KEY); +use constant SSH_PUBLIC_KEY => SSH_KEY_PATH . 'id_rsa.pub'; + push @EXPORT, qw(SSH_PUBLIC_KEY); + #################################################################################################################################### # new #################################################################################################################################### diff --git a/test/lib/pgBackRestTest/Env/Host/HostSftpTest.pm b/test/lib/pgBackRestTest/Env/Host/HostSftpTest.pm new file mode 100644 index 000000000..83efedc6c --- /dev/null +++ b/test/lib/pgBackRestTest/Env/Host/HostSftpTest.pm @@ -0,0 +1,72 @@ +#################################################################################################################################### +# SFTP Test Host +#################################################################################################################################### +package pgBackRestTest::Env::Host::HostSftpTest; +use parent 'pgBackRestTest::Common::HostTest'; + +#################################################################################################################################### +# Perl includes +#################################################################################################################################### +use strict; +use warnings FATAL => qw(all); +use Carp qw(confess); + +use Cwd qw(abs_path); +use Exporter qw(import); + our @EXPORT = qw(); +use File::Basename qw(dirname); +use Storable qw(dclone); + +use pgBackRestDoc::Common::Exception; +use pgBackRestDoc::Common::Ini; +use pgBackRestDoc::Common::Log; +use pgBackRestDoc::ProjectInfo; + +use pgBackRestTest::Common::ContainerTest; +use pgBackRestTest::Common::ExecuteTest; +use pgBackRestTest::Common::HostGroupTest; +use pgBackRestTest::Common::RunTest; +use pgBackRestTest::Common::StorageRepo; +use pgBackRestTest::Common::Wait; +use pgBackRestTest::Env::Host::HostBaseTest; +use pgBackRestTest::Env::Manifest; + +#################################################################################################################################### +# SFTP defaults +#################################################################################################################################### +use constant HOST_SFTP_ACCOUNT => TEST_USER; + push @EXPORT, qw(HOST_SFTP_ACCOUNT); +use constant HOST_SFTP_HOSTKEY_HASH_TYPE => 'sha1'; + push @EXPORT, qw(HOST_SFTP_HOSTKEY_HASH_TYPE); + +#################################################################################################################################### +# new +#################################################################################################################################### +sub new +{ + my $class = shift; # Class name + + # Assign function parameters, defaults, and log debug info + my + ( + $strOperation, + ) = + logDebugParam + ( + __PACKAGE__ . '->new', \@_, + ); + + # Create the host + my $self = $class->SUPER::new( + HOST_SFTP, 'test-' . testRunGet()->vmId() . '-' . HOST_SFTP, containerRepo() . ':' . testRunGet()->vm() . '-test', 'root'); + bless $self, $class; + + # Return from function and log return values if any + return logDebugReturn + ( + $strOperation, + {name => 'self', value => $self, trace => true} + ); +} + +1; diff --git a/test/lib/pgBackRestTest/Env/HostEnvTest.pm b/test/lib/pgBackRestTest/Env/HostEnvTest.pm index 680840d1e..f2a39ddc6 100644 --- a/test/lib/pgBackRestTest/Env/HostEnvTest.pm +++ b/test/lib/pgBackRestTest/Env/HostEnvTest.pm @@ -70,6 +70,10 @@ sub setup { $oHostObject = new pgBackRestTest::Env::Host::HostGcsTest(); } + elsif ($oConfigParam->{strStorage} eq SFTP) + { + $oHostObject = new pgBackRestTest::Env::Host::HostSftpTest(); + } # Get host group my $oHostGroup = hostGroupGet(); @@ -132,7 +136,7 @@ sub setup { $oHostGroup->hostAdd($oHostObject, {rstryHostName => ['pgbackrest-dev.s3.amazonaws.com', 's3.amazonaws.com']}); } - elsif ($oConfigParam->{strStorage} eq AZURE || $oConfigParam->{strStorage} eq GCS) + elsif ($oConfigParam->{strStorage} eq AZURE || $oConfigParam->{strStorage} eq GCS || $oConfigParam->{strStorage} eq SFTP) { $oHostGroup->hostAdd($oHostObject); } @@ -164,7 +168,8 @@ sub setup # If backup host is not defined set it to db-primary else { - $oHostBackup = $strBackupDestination eq HOST_DB_PRIMARY ? $oHostDbPrimary : $oHostDbStandby; + $oHostBackup = $strBackupDestination eq HOST_DB_PRIMARY || $strBackupDestination eq HOST_SFTP ? $oHostDbPrimary : + $oHostDbStandby; } storageRepoCommandSet( @@ -172,7 +177,8 @@ sub setup ' --config=' . $oHostBackup->backrestConfig() . ' --stanza=' . $self->stanza() . ' --log-level-console=off' . ' --log-level-stderr=error' . ($oConfigParam->{strStorage} ne POSIX ? - " --no-repo1-storage-verify-tls --repo1-$oConfigParam->{strStorage}-" . + ($oConfigParam->{strStorage} ne SFTP ? " --no-repo1-storage-verify-tls" : '') . + " --repo1-$oConfigParam->{strStorage}-" . ($oConfigParam->{strStorage} eq GCS ? 'endpoint' : 'host') . "=" . $oHostObject->ipGet() : '') . ($oConfigParam->{strStorage} eq GCS ? ':' . HOST_GCS_PORT : ''), $oConfigParam->{strStorage} eq POSIX ? STORAGE_POSIX : STORAGE_OBJECT); diff --git a/test/lib/pgBackRestTest/Module/Real/RealAllTest.pm b/test/lib/pgBackRestTest/Module/Real/RealAllTest.pm index 78babd415..9f4eeca7d 100644 --- a/test/lib/pgBackRestTest/Module/Real/RealAllTest.pm +++ b/test/lib/pgBackRestTest/Module/Real/RealAllTest.pm @@ -54,13 +54,13 @@ sub run ( {pg => '9.3', dst => 'backup', tls => 0, stg => AZURE, enc => 0, cmp => NONE, rt => 2, bnd => 0, bi => 0}, {pg => '9.4', dst => 'db-standby', tls => 0, stg => POSIX, enc => 1, cmp => LZ4, rt => 1, bnd => 1, bi => 0}, - {pg => '9.5', dst => 'backup', tls => 1, stg => S3, enc => 0, cmp => BZ2, rt => 1, bnd => 0, bi => 1}, + {pg => '9.5', dst => 'backup', tls => 1, stg => GCS, enc => 0, cmp => BZ2, rt => 1, bnd => 0, bi => 1}, {pg => '9.6', dst => 'backup', tls => 0, stg => POSIX, enc => 0, cmp => NONE, rt => 2, bnd => 1, bi => 1}, - {pg => '10', dst => 'db-standby', tls => 1, stg => GCS, enc => 1, cmp => GZ, rt => 2, bnd => 0, bi => 0}, - {pg => '11', dst => 'backup', tls => 1, stg => AZURE, enc => 0, cmp => ZST, rt => 2, bnd => 1, bi => 0}, + {pg => '10', dst => 'sftp-srvr', tls => 0, stg => SFTP, enc => 1, cmp => GZ, rt => 1, bnd => 1, bi => 0}, + {pg => '11', dst => 'backup', tls => 1, stg => AZURE, enc => 0, cmp => ZST, rt => 2, bnd => 0, bi => 0}, {pg => '12', dst => 'backup', tls => 0, stg => S3, enc => 1, cmp => LZ4, rt => 1, bnd => 0, bi => 1}, {pg => '13', dst => 'db-standby', tls => 1, stg => GCS, enc => 0, cmp => ZST, rt => 1, bnd => 1, bi => 1}, - {pg => '14', dst => 'backup', tls => 0, stg => POSIX, enc => 1, cmp => LZ4, rt => 2, bnd => 0, bi => 0}, + {pg => '14', dst => 'sftp-srvr', tls => 0, stg => SFTP, enc => 0, cmp => LZ4, rt => 1, bnd => 1, bi => 0}, {pg => '15', dst => 'db-standby', tls => 0, stg => AZURE, enc => 0, cmp => NONE, rt => 2, bnd => 1, bi => 1}, {pg => '16', dst => 'db-standby', tls => 0, stg => S3, enc => 1, cmp => NONE, rt => 1, bnd => 0, bi => 0}, ) diff --git a/test/src/command/test/build.c b/test/src/command/test/build.c index 5bcabae83..6b92fd3b2 100644 --- a/test/src/command/test/build.c +++ b/test/src/command/test/build.c @@ -552,6 +552,7 @@ testBldUnit(TestBuild *const this) " lib_openssl,\n" " lib_lz4,\n" " lib_pq,\n" + " lib_ssh2,\n" " lib_xml,\n" " lib_yaml,\n" " lib_z,\n" @@ -661,6 +662,7 @@ testBldUnit(TestBuild *const this) strReplace(testC, STRDEF("{[C_TEST_GROUP]}"), groupName()); strReplace(testC, STRDEF("{[C_TEST_GROUP_ID]}"), strNewFmt("%u", groupId())); strReplace(testC, STRDEF("{[C_TEST_USER]}"), userName()); + strReplace(testC, STRDEF("{[C_TEST_USER_LEN]}"), strNewFmt("%zu", strSize(userName()))); strReplace(testC, STRDEF("{[C_TEST_USER_ID]}"), strNewFmt("%u", userId())); // Test id diff --git a/test/src/common/harnessLibSsh2.c b/test/src/common/harnessLibSsh2.c new file mode 100644 index 000000000..3885ee38c --- /dev/null +++ b/test/src/common/harnessLibSsh2.c @@ -0,0 +1,719 @@ +/*********************************************************************************************************************************** +libssh2 Test Harness +***********************************************************************************************************************************/ +#include "build.auto.h" + +#ifdef HAVE_LIBSSH2 + +#include +#include + +#include "common/type/json.h" +#include "common/type/string.h" +#include "common/type/variantList.h" + +#include "common/harnessLibSsh2.h" +#include "common/harnessTest.h" + +/*********************************************************************************************************************************** +libssh2 shim error prefix +***********************************************************************************************************************************/ +#define LIBSSH2_ERROR_PREFIX "LIBSSH2 SHIM ERROR" + +/*********************************************************************************************************************************** +Script that defines how shim functions operate +***********************************************************************************************************************************/ +HrnLibSsh2 hrnLibSsh2Script[1024]; +bool hrnLibSsh2ScriptDone = true; +unsigned int hrnLibSsh2ScriptIdx; + +// If there is a script failure change the behavior of cleanup functions to return immediately so the real error will be reported +// rather than a bogus scripting error during cleanup +bool hrnLibSsh2ScriptFail; +char hrnLibSsh2ScriptError[4096]; + +/*********************************************************************************************************************************** +Set libssh2 script +***********************************************************************************************************************************/ +void +hrnLibSsh2ScriptSet(HrnLibSsh2 *hrnLibSsh2ScriptParam) +{ + if (!hrnLibSsh2ScriptDone) + THROW(AssertError, "previous libssh2 script has not yet completed"); + + if (hrnLibSsh2ScriptParam[0].function == NULL) + THROW(AssertError, "libssh2 script must have entries"); + + // Copy records into local storage + unsigned int copyIdx = 0; + + while (hrnLibSsh2ScriptParam[copyIdx].function != NULL) + { + hrnLibSsh2Script[copyIdx] = hrnLibSsh2ScriptParam[copyIdx]; + copyIdx++; + } + + hrnLibSsh2Script[copyIdx].function = NULL; + hrnLibSsh2ScriptDone = false; + hrnLibSsh2ScriptIdx = 0; +} + +/*********************************************************************************************************************************** +Run libssh2 script +***********************************************************************************************************************************/ +static HrnLibSsh2 * +hrnLibSsh2ScriptRun(const char *const function, const VariantList *const param, const HrnLibSsh2 *const parent) +{ + // If an error has already been thrown then throw the same error again + if (hrnLibSsh2ScriptFail) + THROW(AssertError, hrnLibSsh2ScriptError); + + // Convert params to json for comparison and reporting + String *paramStr = NULL; + + if (param) + { + Variant *const varList = varNewVarLst(param); + + paramStr = jsonFromVar(varList); + varFree(varList); + } + else + paramStr = strNew(); + + // Ensure script has not ended + if (hrnLibSsh2ScriptDone) + { + snprintf( + hrnLibSsh2ScriptError, sizeof(hrnLibSsh2ScriptError), "libssh2 script ended before %s (%s)", function, + strZ(paramStr)); + + TEST_LOG_FMT(LIBSSH2_ERROR_PREFIX ": %s", hrnLibSsh2ScriptError); + hrnLibSsh2ScriptFail = true; + + THROW(AssertError, hrnLibSsh2ScriptError); + } + + // Get current script item + HrnLibSsh2 *result = &hrnLibSsh2Script[hrnLibSsh2ScriptIdx]; + + // Check that expected function was called + if (strcmp(result->function, function) != 0) + { + snprintf( + hrnLibSsh2ScriptError, sizeof(hrnLibSsh2ScriptError), + "libssh2 script [%u] expected function %s (%s) but got %s (%s)", hrnLibSsh2ScriptIdx, result->function, + result->param == NULL ? "" : result->param, function, strZ(paramStr)); + + TEST_LOG_FMT(LIBSSH2_ERROR_PREFIX ": %s", hrnLibSsh2ScriptError); + hrnLibSsh2ScriptFail = true; + + THROW(AssertError, hrnLibSsh2ScriptError); + } + + // Check that parameters match + if ((param != NULL && result->param == NULL) || (param == NULL && result->param != NULL) || + (param != NULL && result->param != NULL && !strEqZ(paramStr, result->param))) + { + snprintf( + hrnLibSsh2ScriptError, sizeof(hrnLibSsh2ScriptError), + "libssh2 script [%u] function '%s', expects param '%s' but got '%s'", + hrnLibSsh2ScriptIdx, result->function, result->param ? result->param : "NULL", param ? strZ(paramStr) : "NULL"); + + TEST_LOG_FMT(LIBSSH2_ERROR_PREFIX ": %s", hrnLibSsh2ScriptError); + hrnLibSsh2ScriptFail = true; + + THROW(AssertError, hrnLibSsh2ScriptError); + } + + // Make sure the session matches with the parent as a sanity check + if (parent != NULL && result->session != parent->session) + { + snprintf( + hrnLibSsh2ScriptError, sizeof(hrnLibSsh2ScriptError), + "libssh2 script [%u] function '%s', expects session '%u' but got '%u'", + hrnLibSsh2ScriptIdx, result->function, result->session, parent->session); + + TEST_LOG_FMT(LIBSSH2_ERROR_PREFIX ": %s", hrnLibSsh2ScriptError); + hrnLibSsh2ScriptFail = true; + + THROW(AssertError, hrnLibSsh2ScriptError); + } + + // Sleep if requested + if (result->sleep > 0) + sleepMSec(result->sleep); + + hrnLibSsh2ScriptIdx++; + + if (hrnLibSsh2Script[hrnLibSsh2ScriptIdx].function == NULL) + hrnLibSsh2ScriptDone = true; + + strFree(paramStr); + + return result; +} + +/*********************************************************************************************************************************** +Shim for libssh2_init +***********************************************************************************************************************************/ +int +libssh2_init(int flags) +{ + HrnLibSsh2 *hrnLibSsh2 = hrnLibSsh2ScriptRun(HRNLIBSSH2_INIT, varLstAdd(varLstNew(), varNewInt(flags)), NULL); + + return hrnLibSsh2->resultInt; +} + +/*********************************************************************************************************************************** +Shim for libssh2_session_init +***********************************************************************************************************************************/ +LIBSSH2_SESSION * +libssh2_session_init_ex( + LIBSSH2_ALLOC_FUNC((*myalloc)), LIBSSH2_FREE_FUNC((*myfree)), LIBSSH2_REALLOC_FUNC((*myrealloc)), void *abstract) +{ + // All of these should always be the default NULL + if (myalloc != NULL && myfree != NULL && myrealloc != NULL && abstract != NULL) + { + snprintf( + hrnLibSsh2ScriptError, sizeof(hrnLibSsh2ScriptError), + "libssh2 script function 'libssh2_session_init_ex', expects all params to be NULL"); + THROW(AssertError, hrnLibSsh2ScriptError); + } + + HrnLibSsh2 *hrnLibSsh2 = hrnLibSsh2ScriptRun( + HRNLIBSSH2_SESSION_INIT_EX, + varLstAdd( + varLstAdd( + varLstAdd( + varLstAdd( + varLstNew(), varNewStr(NULL)), + varNewStr(NULL)), + varNewStr(NULL)), + varNewStr(NULL)), + NULL); + + return hrnLibSsh2->resultNull ? NULL : (LIBSSH2_SESSION *)hrnLibSsh2; +} + +/*********************************************************************************************************************************** +Shim for libssh2_session_set_blocking +***********************************************************************************************************************************/ +void +libssh2_session_set_blocking(LIBSSH2_SESSION *session, int blocking) +{ + if (session == NULL) + { + snprintf( + hrnLibSsh2ScriptError, sizeof(hrnLibSsh2ScriptError), + "libssh2 script function 'libssh2_session_set_blocking', expects session to be not NULL"); + THROW(AssertError, hrnLibSsh2ScriptError); + } + + if (!(INT_MIN <= blocking && blocking <= INT_MAX)) + { + snprintf( + hrnLibSsh2ScriptError, sizeof(hrnLibSsh2ScriptError), + "libssh2 script function 'libssh2_session_set_blocking', expects blocking to be an integer value"); + THROW(AssertError, hrnLibSsh2ScriptError); + } + + return; +} + +/*********************************************************************************************************************************** +Shim for libssh2_session_handshake +***********************************************************************************************************************************/ +int +libssh2_session_handshake(LIBSSH2_SESSION *session, libssh2_socket_t sock) +{ + return hrnLibSsh2ScriptRun( + HRNLIBSSH2_SESSION_HANDSHAKE, varLstAdd(varLstNew(), varNewInt(sock)), (HrnLibSsh2 *)session)->resultInt; +} + +/*********************************************************************************************************************************** +Shim for libssh2_hostkey_hash +***********************************************************************************************************************************/ +const char * +libssh2_hostkey_hash(LIBSSH2_SESSION *session, int hash_type) +{ + HrnLibSsh2 *hrnLibSsh2 = NULL; + + MEM_CONTEXT_TEMP_BEGIN() + { + hrnLibSsh2 = hrnLibSsh2ScriptRun( + HRNLIBSSH2_HOSTKEY_HASH, varLstAdd(varLstNew(), varNewInt(hash_type)), (HrnLibSsh2 *)session); + } + MEM_CONTEXT_TEMP_END(); + + return hrnLibSsh2->resultNull ? NULL : (const char *)hrnLibSsh2->resultZ; +} + +/*********************************************************************************************************************************** +Shim for libssh2_userauth_publickey_fromfile_ex +***********************************************************************************************************************************/ +int +libssh2_userauth_publickey_fromfile_ex( + LIBSSH2_SESSION *session, const char *username, unsigned int ousername_len, const char *publickey, const char *privatekey, + const char *passphrase) +{ + HrnLibSsh2 *hrnLibSsh2 = NULL; + + MEM_CONTEXT_TEMP_BEGIN() + { + hrnLibSsh2 = hrnLibSsh2ScriptRun( + HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX, + varLstAdd( + varLstAdd( + varLstAdd( + varLstAdd( + varLstAdd( + varLstNew(), varNewStrZ(username)), + varNewUInt(ousername_len)), + varNewStrZ(publickey)), + varNewStrZ(privatekey)), + varNewStrZ(passphrase)), + (HrnLibSsh2 *)session); + } + MEM_CONTEXT_TEMP_END(); + + return hrnLibSsh2->resultInt; +} + +/*********************************************************************************************************************************** +Shim for libssh2_sftp_init +***********************************************************************************************************************************/ +LIBSSH2_SFTP * +libssh2_sftp_init(LIBSSH2_SESSION *session) +{ + HrnLibSsh2 *hrnLibSsh2 = hrnLibSsh2ScriptRun(HRNLIBSSH2_SFTP_INIT, NULL, (HrnLibSsh2 *)session); + + return hrnLibSsh2->resultNull ? NULL : (LIBSSH2_SFTP *)hrnLibSsh2; +} + +/*********************************************************************************************************************************** +Shim for libssh2_sftp_close_handle +***********************************************************************************************************************************/ +int +libssh2_sftp_close_handle(LIBSSH2_SFTP_HANDLE *handle) +{ + return hrnLibSsh2ScriptRun(HRNLIBSSH2_SFTP_CLOSE_HANDLE, NULL, (HrnLibSsh2 *)handle)->resultInt; +} + +/*********************************************************************************************************************************** +Shim for libssh2_sftp_shutdown +***********************************************************************************************************************************/ +int +libssh2_sftp_shutdown(LIBSSH2_SFTP *sftp) +{ + return hrnLibSsh2ScriptRun(HRNLIBSSH2_SFTP_SHUTDOWN, NULL, (HrnLibSsh2 *)sftp)->resultInt; +} + +/*********************************************************************************************************************************** +Shim for libssh2_session_disconnect_ex +***********************************************************************************************************************************/ +int +libssh2_session_disconnect_ex(LIBSSH2_SESSION *session, int reason, const char *description, const char *lang) +{ + HrnLibSsh2 *hrnLibSsh2 = NULL; + + MEM_CONTEXT_TEMP_BEGIN() + { + hrnLibSsh2 = hrnLibSsh2ScriptRun( + HRNLIBSSH2_SESSION_DISCONNECT_EX, + varLstAdd( + varLstAdd( + varLstAdd( + varLstNew(), varNewInt(reason)), + varNewStrZ(description)), + varNewStrZ(lang)), + (HrnLibSsh2 *)session); + } + MEM_CONTEXT_TEMP_END(); + + return hrnLibSsh2->resultInt; +} + +/*********************************************************************************************************************************** +Shim for int libssh2_session_free +***********************************************************************************************************************************/ +int +libssh2_session_free(LIBSSH2_SESSION *session) +{ + return hrnLibSsh2ScriptRun(HRNLIBSSH2_SESSION_FREE, NULL, (HrnLibSsh2 *)session)->resultInt; +} + +/*********************************************************************************************************************************** +Shim for libssh2_sftp_stat_ex +***********************************************************************************************************************************/ +int +libssh2_sftp_stat_ex( + LIBSSH2_SFTP *sftp, const char *path, unsigned int path_len, int stat_type, LIBSSH2_SFTP_ATTRIBUTES *attrs) +{ + // Avoid compiler complaining of unused param. Not passing to hrnLibSsh2ScriptRun, as parameter will vary depending on where + // tests are being run. + // + // Could we utilize test.c/build.c to calculate/define this and other length params? + (void)path_len; + + HrnLibSsh2 *hrnLibSsh2 = NULL; + + MEM_CONTEXT_TEMP_BEGIN() + { + hrnLibSsh2 = hrnLibSsh2ScriptRun( + HRNLIBSSH2_SFTP_STAT_EX, + varLstAdd( + varLstAdd( + varLstNew(), varNewStrZ(path)), + varNewInt(stat_type)), + (HrnLibSsh2 *)sftp); + } + MEM_CONTEXT_TEMP_END(); + + if (attrs == NULL) + THROW(AssertError, "attrs is NULL"); + + attrs->flags = 0; + attrs->flags |= (unsigned long)hrnLibSsh2->flags; + + attrs->permissions = 0; + attrs->permissions |= (unsigned long)hrnLibSsh2->attrPerms; + + attrs->mtime = (unsigned long)hrnLibSsh2->mtime; + attrs->uid = (unsigned long)hrnLibSsh2->uid; + attrs->gid = (unsigned long)hrnLibSsh2->gid; + attrs->filesize = hrnLibSsh2->filesize; + + return hrnLibSsh2->resultInt; +} + +/*********************************************************************************************************************************** +Shim for libssh2_sftp_last_error +***********************************************************************************************************************************/ +unsigned long +libssh2_sftp_last_error(LIBSSH2_SFTP *sftp) +{ + return (unsigned long)hrnLibSsh2ScriptRun(HRNLIBSSH2_SFTP_LAST_ERROR, NULL, (HrnLibSsh2 *)sftp)->resultUInt; +} + +/*********************************************************************************************************************************** +Shim for libssh2_sftp_symlink_ex +***********************************************************************************************************************************/ +int +libssh2_sftp_symlink_ex( + LIBSSH2_SFTP *sftp, const char *path, unsigned int path_len, char *target, unsigned int target_len, int link_type) +{ + // Avoid compiler complaining of unused param. Not passing to hrnLibSsh2ScriptRun, as parameter will vary depending on where + // tests are being run. + (void)path_len; + (void)target_len; + + HrnLibSsh2 *hrnLibSsh2 = NULL; + + MEM_CONTEXT_TEMP_BEGIN() + { + hrnLibSsh2 = hrnLibSsh2ScriptRun( + HRNLIBSSH2_SFTP_SYMLINK_EX, + varLstAdd( + varLstAdd( + varLstAdd( + varLstNew(), varNewStrZ(path)), + varNewStrZ(target)), + varNewInt(link_type)), + (HrnLibSsh2 *)sftp); + } + MEM_CONTEXT_TEMP_END(); + + int rc = 0; + + switch (link_type) + { + case LIBSSH2_SFTP_READLINK: + case LIBSSH2_SFTP_REALPATH: + if (hrnLibSsh2->symlinkExTarget != NULL) + { + if (strSize(hrnLibSsh2->symlinkExTarget) < PATH_MAX) + strncpy(target, strZ(hrnLibSsh2->symlinkExTarget), strSize(hrnLibSsh2->symlinkExTarget)); + else + THROW_FMT(AssertError, "symlinkExTarget too large for target buffer"); + } + + rc = hrnLibSsh2->resultInt != 0 ? hrnLibSsh2->resultInt : (int)strSize(hrnLibSsh2->symlinkExTarget); + break; + + default: + THROW_FMT(AssertError, "UNKNOWN link_type"); + break; + } + + return rc; +} + +/*********************************************************************************************************************************** +Shim for libssh2_sftp_open_ex +***********************************************************************************************************************************/ +LIBSSH2_SFTP_HANDLE * +libssh2_sftp_open_ex( + LIBSSH2_SFTP *sftp, const char *filename, unsigned int filename_len, unsigned long flags, long mode, int open_type) +{ + // To avoid compiler complaining of unused param. Not passing to hrnLibSsh2ScriptRun, as parameter will vary depending on where + // tests are being run. + (void)filename_len; + + HrnLibSsh2 *hrnLibSsh2 = NULL; + + MEM_CONTEXT_TEMP_BEGIN() + { + hrnLibSsh2 = hrnLibSsh2ScriptRun( + HRNLIBSSH2_SFTP_OPEN_EX, + varLstAdd( + varLstAdd( + varLstAdd( + varLstAdd( + varLstNew(), varNewStrZ(filename)), + varNewUInt64(flags)), + varNewInt64(mode)), + varNewInt(open_type)), + (HrnLibSsh2 *)sftp); + } + MEM_CONTEXT_TEMP_END(); + + return hrnLibSsh2->resultNull ? NULL : (LIBSSH2_SFTP_HANDLE *)hrnLibSsh2; +} + +/*********************************************************************************************************************************** +Shim for libssh2_sftp_readdir_ex +***********************************************************************************************************************************/ +int +libssh2_sftp_readdir_ex( + LIBSSH2_SFTP_HANDLE *handle, char *buffer, size_t buffer_maxlen, char *longentry, size_t longentry_maxlen, + LIBSSH2_SFTP_ATTRIBUTES *attrs) +{ + if (attrs == NULL) + THROW_FMT(AssertError, "attrs is NULL"); + + HrnLibSsh2 *hrnLibSsh2 = NULL; + + MEM_CONTEXT_TEMP_BEGIN() + { + hrnLibSsh2 = hrnLibSsh2ScriptRun( + HRNLIBSSH2_SFTP_READDIR_EX, + varLstAdd( + varLstAdd( + varLstAdd( + varLstAdd( + varLstNew(), varNewStrZ(buffer)), + varNewUInt64(buffer_maxlen)), + varNewStrZ(longentry)), + varNewUInt64(longentry_maxlen)), + (HrnLibSsh2 *)handle); + } + MEM_CONTEXT_TEMP_END(); + + if (hrnLibSsh2->fileName != NULL) + strncpy(buffer, strZ(hrnLibSsh2->fileName), buffer_maxlen); + + return hrnLibSsh2->resultInt; +} + +/*********************************************************************************************************************************** +Shim for libssh2_session_last_errno +***********************************************************************************************************************************/ +int +libssh2_session_last_errno(LIBSSH2_SESSION *session) +{ + return hrnLibSsh2ScriptRun(HRNLIBSSH2_SESSION_LAST_ERRNO, NULL, (HrnLibSsh2 *)session)->resultInt; +} + +/*********************************************************************************************************************************** +Shim for libssh2_sftp_fsync +***********************************************************************************************************************************/ +int +libssh2_sftp_fsync(LIBSSH2_SFTP_HANDLE *handle) +{ + return hrnLibSsh2ScriptRun(HRNLIBSSH2_SFTP_FSYNC, NULL, (HrnLibSsh2 *)handle)->resultInt; +} + +/*********************************************************************************************************************************** +Shim for libssh2_sftp_mkdir_ex +***********************************************************************************************************************************/ +int +libssh2_sftp_mkdir_ex(LIBSSH2_SFTP *sftp, const char *path, unsigned int path_len, long mode) +{ + // To avoid compiler complaining of unused param. Not passing to hrnLibSsh2ScriptRun, as parameter will vary depending on where + // tests are being run. + (void)path_len; + + HrnLibSsh2 *hrnLibSsh2 = NULL; + + MEM_CONTEXT_TEMP_BEGIN() + { + hrnLibSsh2 = hrnLibSsh2ScriptRun( + HRNLIBSSH2_SFTP_MKDIR_EX, + varLstAdd( + varLstAdd( + varLstNew(), varNewStrZ(path)), + varNewInt64(mode)), + (HrnLibSsh2 *)sftp); + } + MEM_CONTEXT_TEMP_END(); + + return hrnLibSsh2->resultInt; +} + +/*********************************************************************************************************************************** +Shim for libssh2_sftp_read +***********************************************************************************************************************************/ +ssize_t +libssh2_sftp_read(LIBSSH2_SFTP_HANDLE *handle, char *buffer, size_t buffer_maxlen) +{ + // We don't pass buffer to hrnLibSsh2ScriptRun. The first call for each invocation passes buffer with random data, which is + // an issue for sftpTest.c. + HrnLibSsh2 *hrnLibSsh2 = NULL; + + MEM_CONTEXT_TEMP_BEGIN() + { + hrnLibSsh2 = hrnLibSsh2ScriptRun( + HRNLIBSSH2_SFTP_READ, + varLstAdd( + varLstNew(), varNewUInt64(buffer_maxlen)), + (HrnLibSsh2 *)handle); + } + MEM_CONTEXT_TEMP_END(); + + // copy read into buffer + if (hrnLibSsh2->readBuffer != NULL) + strncpy(buffer, strZ(hrnLibSsh2->readBuffer), strSize(hrnLibSsh2->readBuffer)); + + // number of bytes populated + return hrnLibSsh2->resultInt; +} + +/*********************************************************************************************************************************** +Shim for libssh2_sftp_rename_ex +***********************************************************************************************************************************/ +int +libssh2_sftp_rename_ex( + LIBSSH2_SFTP *sftp, const char *source_filename, unsigned int source_filename_len, const char *dest_filename, + unsigned int dest_filename_len, long flags) +{ + // To avoid compiler complaining of unused param. Not passing to hrnLibSsh2ScriptRun, as parameter will vary depending on where + // tests are being run. + (void)source_filename_len; + (void)dest_filename_len; + + HrnLibSsh2 *hrnLibSsh2 = NULL; + + MEM_CONTEXT_TEMP_BEGIN() + { + hrnLibSsh2 = hrnLibSsh2ScriptRun( + HRNLIBSSH2_SFTP_RENAME_EX, + varLstAdd( + varLstAdd( + varLstAdd( + varLstNew(), varNewStrZ(source_filename)), + varNewStrZ(dest_filename)), + varNewInt64(flags)), + (HrnLibSsh2 *)sftp); + } + MEM_CONTEXT_TEMP_END(); + + return hrnLibSsh2->resultInt; +} + +/*********************************************************************************************************************************** +Shim for libssh2_sftp_rmdir_ex +***********************************************************************************************************************************/ +int +libssh2_sftp_rmdir_ex(LIBSSH2_SFTP *sftp, const char *path, unsigned int path_len) +{ + // Avoid compiler complaining of unused param. Not passing to hrnLibSsh2ScriptRun, as parameter will vary depending on where + // tests are being run. + (void)path_len; + + HrnLibSsh2 *hrnLibSsh2 = NULL; + + MEM_CONTEXT_TEMP_BEGIN() + { + hrnLibSsh2 = hrnLibSsh2ScriptRun( + HRNLIBSSH2_SFTP_RMDIR_EX, + varLstAdd( + varLstNew(), varNewStrZ(path)), + (HrnLibSsh2 *)sftp); + } + MEM_CONTEXT_TEMP_END(); + + return hrnLibSsh2->resultInt; +} + +/*********************************************************************************************************************************** +Shim for libssh2_sftp_seek64 +***********************************************************************************************************************************/ +void +libssh2_sftp_seek64(LIBSSH2_SFTP_HANDLE *handle, libssh2_uint64_t offset) +{ + MEM_CONTEXT_TEMP_BEGIN() + { + hrnLibSsh2ScriptRun( + HRNLIBSSH2_SFTP_SEEK64, + varLstAdd( + varLstNew(), varNewUInt64(offset)), + (HrnLibSsh2 *)handle); + } + MEM_CONTEXT_TEMP_END(); + + return; +} + +/*********************************************************************************************************************************** +Shim for libssh2_sftp_unlink_ex +***********************************************************************************************************************************/ +int +libssh2_sftp_unlink_ex(LIBSSH2_SFTP *sftp, const char *filename, unsigned int filename_len) +{ + // Avoid compiler complaining of unused param. Not passing to hrnLibSsh2ScriptRun, as parameter will vary depending on where + // tests are being run. + (void)filename_len; + + HrnLibSsh2 *hrnLibSsh2 = NULL; + + MEM_CONTEXT_TEMP_BEGIN() + { + hrnLibSsh2 = hrnLibSsh2ScriptRun( + HRNLIBSSH2_SFTP_UNLINK_EX, + varLstAdd( + varLstNew(), varNewStrZ(filename)), + (HrnLibSsh2 *)sftp); + } + MEM_CONTEXT_TEMP_END(); + + return hrnLibSsh2->resultInt; +} + +/*********************************************************************************************************************************** +Shim for libssh2_sftp_write +***********************************************************************************************************************************/ +ssize_t +libssh2_sftp_write(LIBSSH2_SFTP_HANDLE *handle, const char *buffer, size_t count) +{ + // We don't pass buffer to hrnLibSsh2ScriptRun. The first call for each invocation passes buffer with random data, which is + // an issue for sftpTest.c. + (void)buffer; + + HrnLibSsh2 *hrnLibSsh2 = NULL; + + MEM_CONTEXT_TEMP_BEGIN() + { + hrnLibSsh2 = hrnLibSsh2ScriptRun( + HRNLIBSSH2_SFTP_WRITE, + varLstAdd( + varLstNew(), varNewUInt64(count)), + (HrnLibSsh2 *)handle); + } + MEM_CONTEXT_TEMP_END(); + + // Return number of bytes written + return hrnLibSsh2->resultInt; +} + +#endif // HAVE_LIBSSH2 diff --git a/test/src/common/harnessLibSsh2.h b/test/src/common/harnessLibSsh2.h new file mode 100644 index 000000000..301bb0a84 --- /dev/null +++ b/test/src/common/harnessLibSsh2.h @@ -0,0 +1,122 @@ +/*********************************************************************************************************************************** +libssh2 Test Harness + +Scripted testing for libssh2 so exact results can be returned for unit testing. See sftp unit tests for usage examples. +***********************************************************************************************************************************/ +#ifndef TEST_COMMON_HARNESS_LIBSSH2_H +#define TEST_COMMON_HARNESS_LIBSSH2_H + +#ifdef HAVE_LIBSSH2 + +#ifndef HARNESS_LIBSSH2_REAL + +#include +#include +#include + +#include "common/macro.h" +#include "common/time.h" +#include "version.h" + +/*********************************************************************************************************************************** +libssh2 authorization constants +***********************************************************************************************************************************/ +#define KEYPRIV STRDEF("/home/" TEST_USER "/.ssh/id_rsa") +#define KEYPUB STRDEF("/home/" TEST_USER "/.ssh/id_rsa.pub") +#define KEYPRIV_CSTR "/home/" TEST_USER "/.ssh/id_rsa" +#define KEYPUB_CSTR "/home/" TEST_USER "/.ssh/id_rsa.pub" + +/*********************************************************************************************************************************** +Function constants +***********************************************************************************************************************************/ +#define HRNLIBSSH2_HOSTKEY_HASH "libssh2_hostkey_hash" +#define HRNLIBSSH2_INIT "libssh2_init" +#define HRNLIBSSH2_SESSION_DISCONNECT_EX "libssh2_session_disconnect_ex" +#define HRNLIBSSH2_SESSION_FREE "libssh2_session_free" +#define HRNLIBSSH2_SESSION_HANDSHAKE "libssh2_session_handshake" +#define HRNLIBSSH2_SESSION_INIT_EX "libssh2_session_init_ex" +#define HRNLIBSSH2_SESSION_LAST_ERRNO "libssh2_session_last_errno" +#define HRNLIBSSH2_SESSION_LAST_ERROR "libssh2_session_last_error" +#define HRNLIBSSH2_SESSION_SET_BLOCKING "libssh2_session_set_blocking" +#define HRNLIBSSH2_SFTP_CLOSE_HANDLE "libssh2_sftp_close_handle" +#define HRNLIBSSH2_SFTP_FSYNC "libssh2_sftp_fsync" +#define HRNLIBSSH2_SFTP_INIT "libssh2_sftp_init" +#define HRNLIBSSH2_SFTP_LAST_ERROR "libssh2_sftp_last_error" +#define HRNLIBSSH2_SFTP_MKDIR_EX "libssh2_sftp_mkdir_ex" +#define HRNLIBSSH2_SFTP_OPEN_EX "libssh2_sftp_open_ex" +#define HRNLIBSSH2_SFTP_READ "libssh2_sftp_read" +#define HRNLIBSSH2_SFTP_READDIR_EX "libssh2_sftp_readdir_ex" +#define HRNLIBSSH2_SFTP_RENAME_EX "libssh2_sftp_rename_ex" +#define HRNLIBSSH2_SFTP_RMDIR_EX "libssh2_sftp_rmdir_ex" +#define HRNLIBSSH2_SFTP_SEEK64 "libssh2_sftp_seek64" +#define HRNLIBSSH2_SFTP_SHUTDOWN "libssh2_sftp_shutdown" +#define HRNLIBSSH2_SFTP_STAT_EX "libssh2_sftp_stat_ex" +#define HRNLIBSSH2_SFTP_SYMLINK_EX "libssh2_sftp_symlink_ex" +#define HRNLIBSSH2_SFTP_UNLINK_EX "libssh2_sftp_unlink_ex" +#define HRNLIBSSH2_SFTP_WRITE "libssh2_sftp_write" +#define HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX "libssh2_userauth_publickey_fromfile_ex" + +/*********************************************************************************************************************************** +Macros for defining groups of functions that implement commands +***********************************************************************************************************************************/ +// Set of functions mimicking libssh2 inititialization and authorization +#define HRNLIBSSH2_MACRO_STARTUP() \ + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = 0}, \ + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, \ + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = 0}, \ + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = "12345678910123456789"}, \ + {.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX, \ + .param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_CSTR "\",\"" KEYPRIV_CSTR "\",null]", \ + .resultInt = 0}, \ + {.function = HRNLIBSSH2_SFTP_INIT} + +// Set of functions mimicking libssh2 shutdown and disconnect +#define HRNLIBSSH2_MACRO_SHUTDOWN() \ + {.function = HRNLIBSSH2_SFTP_SHUTDOWN, .resultInt = 0}, \ + {.function = HRNLIBSSH2_SESSION_DISCONNECT_EX, .param ="[11,\"pgbackrest instance shutdown\",\"\"]", .resultInt = 0}, \ + {.function = HRNLIBSSH2_SESSION_FREE, .resultInt = 0}, \ + {.function = NULL} \ + +// older systems do not support LIBSSH2_HOSTKEY_HASH_SHA256 +#ifdef LIBSSH2_HOSTKEY_HASH_SHA256 +#define HOSTKEY_HASH_ENTRY() \ + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[3]", .resultZ = "12345678910123456789"} +#else +#define HOSTKEY_HASH_ENTRY() \ + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = "12345678910123456789"} +#endif + +/*********************************************************************************************************************************** +Structure for scripting libssh2 responses +***********************************************************************************************************************************/ +typedef struct HrnLibSsh2 +{ + unsigned int session; // Session number when multiple sessions are run concurrently + const char *function; // Function call expected + const char *param; // Params expected by the function for verification + int resultInt; // Int result value + uint64_t resultUInt; // UInt result value + const char *resultZ; // Zero-terminated result value + bool resultNull; // Return null from function that normally returns a struct ptr + uint64_t flags; // libssh2 flags + uint64_t attrPerms; // libssh2 attr perms + uint64_t atime, mtime; // libssh2 timestamps + uint64_t uid, gid; // libssh2 uid/gid + uint64_t filesize; // libssh2 filesize + uint64_t offset; // libssh2 seek offset + const String *symlinkExTarget; // libssh2_sftp_symlink_ex target + const String *fileName; // libssh2_readdir* libssh2_stat* filename + const String *readBuffer; // what to copy into read buffer + TimeMSec sleep; // Sleep specified milliseconds before returning from function +} HrnLibSsh2; + +/*********************************************************************************************************************************** +Functions +***********************************************************************************************************************************/ +void hrnLibSsh2ScriptSet(HrnLibSsh2 *hrnLibSsh2ScriptParam); + +#endif // HARNESS_LIBSSH2_REAL + +#endif // HAVE_LIBSSH2 + +#endif // TEST_COMMON_HARNESS_LIBSSH2_H diff --git a/test/src/common/harnessSocket.c b/test/src/common/harnessSocket.c new file mode 100644 index 000000000..3b98febf2 --- /dev/null +++ b/test/src/common/harnessSocket.c @@ -0,0 +1,75 @@ +/*********************************************************************************************************************************** +Harness for Io Testing +***********************************************************************************************************************************/ +#include "build.auto.h" + +#include "common/harnessConfig.h" +#include "common/harnessDebug.h" +#include "common/harnessSocket.h" + +/*********************************************************************************************************************************** +Include shimmed C modules +***********************************************************************************************************************************/ +{[SHIM_MODULE]} + +/*********************************************************************************************************************************** +Shim install state +***********************************************************************************************************************************/ +static struct +{ + // Local process shims + bool localShimSckClientOpen; +} hrnIoStatic; + +/*********************************************************************************************************************************** +Shim sckClientOpen() +***********************************************************************************************************************************/ +IoSession * +sckClientOpen(THIS_VOID) +{ + THIS(SocketClient); + + FUNCTION_HARNESS_BEGIN(); + FUNCTION_HARNESS_PARAM(SOCKET_CLIENT, this); + FUNCTION_HARNESS_END(); + + ASSERT(this != NULL); + + IoSession *result = NULL; + + // When shim is installed create IoSession with a known fd + if (hrnIoStatic.localShimSckClientOpen) + { + result = sckSessionNew(ioSessionRoleClient, HRN_SCK_FILE_DESCRIPTOR, this->host, this->port, this->timeoutSession); + + // Remove the callback so we will not try to close the fake descriptor + memContextCallbackClear(objMemContext(((IoSessionPub *)result)->driver)); + } + // Else call normal function + else + result = sckClientOpen_SHIMMED(this); + + FUNCTION_HARNESS_RETURN(IO_SESSION, result); +} + +/**********************************************************************************************************************************/ +void +hrnSckClientOpenShimInstall(void) +{ + FUNCTION_HARNESS_VOID(); + + hrnIoStatic.localShimSckClientOpen = true; + + FUNCTION_HARNESS_RETURN_VOID(); +} + +/**********************************************************************************************************************************/ +void +hrnSckClientOpenShimUninstall(void) +{ + FUNCTION_HARNESS_VOID(); + + hrnIoStatic.localShimSckClientOpen = false; + + FUNCTION_HARNESS_RETURN_VOID(); +} diff --git a/test/src/common/harnessSocket.h b/test/src/common/harnessSocket.h new file mode 100644 index 000000000..a022def63 --- /dev/null +++ b/test/src/common/harnessSocket.h @@ -0,0 +1,16 @@ +/*********************************************************************************************************************************** +Harness for Io Testing +***********************************************************************************************************************************/ + +/*********************************************************************************************************************************** +Constants +***********************************************************************************************************************************/ +// Arbitrary value for the shimmed socket file descriptor +#define HRN_SCK_FILE_DESCRIPTOR 1163581 + +/*********************************************************************************************************************************** +Functions +***********************************************************************************************************************************/ +// Install/uninstall the shims for testing +void hrnSckClientOpenShimInstall(void); +void hrnSckClientOpenShimUninstall(void); diff --git a/test/src/module/command/helpTest.c b/test/src/module/command/helpTest.c index 111d9ed54..8c152624b 100644 --- a/test/src/module/command/helpTest.c +++ b/test/src/module/command/helpTest.c @@ -202,132 +202,142 @@ testRun(void) "\n" "Command Options:\n" "\n" - " --archive-mode preserve or disable archiving on restored\n" - " cluster [default=preserve]\n" - " --db-exclude restore excluding the specified databases\n" - " --db-include restore only specified databases\n" - " [current=db1, db2]\n" - " --force force a restore [default=n]\n" - " --link-all restore all symlinks [default=n]\n" - " --link-map modify the destination of a symlink\n" - " [current=/link1=/dest1, /link2=/dest2]\n" - " --recovery-option set an option in recovery.conf\n" - " --set backup set to restore [default=latest]\n" - " --tablespace-map restore a tablespace into the specified\n" - " directory\n" - " --tablespace-map-all restore all tablespaces into the specified\n" - " directory\n" - " --target recovery target\n" - " --target-action action to take when recovery target is\n" - " reached [default=pause]\n" - " --target-exclusive stop just before the recovery target is\n" - " reached [default=n]\n" - " --target-timeline recover along a timeline\n" - " --type recovery type [default=default]\n" + " --archive-mode preserve or disable archiving on restored\n" + " cluster [default=preserve]\n" + " --db-exclude restore excluding the specified databases\n" + " --db-include restore only specified databases\n" + " [current=db1, db2]\n" + " --force force a restore [default=n]\n" + " --link-all restore all symlinks [default=n]\n" + " --link-map modify the destination of a symlink\n" + " [current=/link1=/dest1, /link2=/dest2]\n" + " --recovery-option set an option in recovery.conf\n" + " --set backup set to restore [default=latest]\n" + " --tablespace-map restore a tablespace into the specified\n" + " directory\n" + " --tablespace-map-all restore all tablespaces into the\n" + " specified directory\n" + " --target recovery target\n" + " --target-action action to take when recovery target is\n" + " reached [default=pause]\n" + " --target-exclusive stop just before the recovery target is\n" + " reached [default=n]\n" + " --target-timeline recover along a timeline\n" + " --type recovery type [default=default]\n" "\n" "General Options:\n" "\n" - " --buffer-size buffer size for I/O operations\n" - " [current=32768, default=1MiB]\n" - " --cmd pgBackRest command\n" - " [default=/path/to/pgbackrest]\n" - " --cmd-ssh SSH client command [default=ssh]\n" - " --compress-level-network network compression level [default=3]\n" - " --config pgBackRest configuration file\n" - " [default=/etc/pgbackrest/pgbackrest.conf]\n" - " --config-include-path path to additional pgBackRest configuration\n" - " files [default=/etc/pgbackrest/conf.d]\n" - " --config-path base path of pgBackRest configuration files\n" - " [default=/etc/pgbackrest]\n" - " --delta restore or backup using checksums\n" - " [default=n]\n" - " --io-timeout I/O timeout [default=60]\n" - " --lock-path path where lock files are stored\n" - " [default=/tmp/pgbackrest]\n" - " --neutral-umask use a neutral umask [default=y]\n" - " --process-max max processes to use for compress/transfer\n" - " [default=1]\n" - " --protocol-timeout protocol timeout [default=1830]\n" - " --sck-keep-alive keep-alive enable [default=y]\n" - " --stanza defines the stanza\n" - " --tcp-keep-alive-count keep-alive count\n" - " --tcp-keep-alive-idle keep-alive idle time\n" - " --tcp-keep-alive-interval keep-alive interval time\n" + " --buffer-size buffer size for I/O operations\n" + " [current=32768, default=1MiB]\n" + " --cmd pgBackRest command\n" + " [default=/path/to/pgbackrest]\n" + " --cmd-ssh SSH client command [default=ssh]\n" + " --compress-level-network network compression level [default=3]\n" + " --config pgBackRest configuration file\n" + " [default=/etc/pgbackrest/pgbackrest.conf]\n" + " --config-include-path path to additional pgBackRest\n" + " configuration files\n" + " [default=/etc/pgbackrest/conf.d]\n" + " --config-path base path of pgBackRest configuration\n" + " files [default=/etc/pgbackrest]\n" + " --delta restore or backup using checksums\n" + " [default=n]\n" + " --io-timeout I/O timeout [default=60]\n" + " --lock-path path where lock files are stored\n" + " [default=/tmp/pgbackrest]\n" + " --neutral-umask use a neutral umask [default=y]\n" + " --process-max max processes to use for\n" + " compress/transfer [default=1]\n" + " --protocol-timeout protocol timeout [default=1830]\n" + " --sck-keep-alive keep-alive enable [default=y]\n" + " --stanza defines the stanza\n" + " --tcp-keep-alive-count keep-alive count\n" + " --tcp-keep-alive-idle keep-alive idle time\n" + " --tcp-keep-alive-interval keep-alive interval time\n" "\n" "Log Options:\n" "\n" - " --log-level-console level for console logging [default=warn]\n" - " --log-level-file level for file logging [default=info]\n" - " --log-level-stderr level for stderr logging [default=warn]\n" - " --log-path path where log files are stored\n" - " [default=/var/log/pgbackrest]\n" - " --log-subprocess enable logging in subprocesses [default=n]\n" - " --log-timestamp enable timestamp in logging [default=y]\n" + " --log-level-console level for console logging [default=warn]\n" + " --log-level-file level for file logging [default=info]\n" + " --log-level-stderr level for stderr logging [default=warn]\n" + " --log-path path where log files are stored\n" + " [default=/var/log/pgbackrest]\n" + " --log-subprocess enable logging in subprocesses [default=n]\n" + " --log-timestamp enable timestamp in logging [default=y]\n" "\n", "Repository Options:\n" "\n" - " --repo set repository\n" - " --repo-azure-account azure repository account\n" - " --repo-azure-container azure repository container\n" - " --repo-azure-endpoint azure repository endpoint\n" - " [default=blob.core.windows.net]\n" - " --repo-azure-key azure repository key\n" - " --repo-azure-key-type azure repository key type [default=shared]\n" - " --repo-azure-uri-style azure URI Style [default=host]\n" - " --repo-cipher-pass repository cipher passphrase\n" - " [current=]\n" - " --repo-cipher-type cipher used to encrypt the repository\n" - " [current=aes-256-cbc, default=none]\n" - " --repo-gcs-bucket GCS repository bucket\n" - " --repo-gcs-endpoint GCS repository endpoint\n" - " [default=storage.googleapis.com]\n" - " --repo-gcs-key GCS repository key\n" - " --repo-gcs-key-type GCS repository key type [default=service]\n" - " --repo-host repository host when operating remotely\n" - " [current=backup.example.net]\n" - " --repo-host-ca-file repository host certificate authority file\n" - " --repo-host-ca-path repository host certificate authority path\n" - " --repo-host-cert-file repository host certificate file\n" - " --repo-host-cmd repository host pgBackRest command\n" - " [default=/path/to/pgbackrest]\n" - " --repo-host-config pgBackRest repository host configuration\n" - " file\n" - " [default=/etc/pgbackrest/pgbackrest.conf]\n" - " --repo-host-config-include-path pgBackRest repository host configuration\n" - " include path\n" - " [default=/etc/pgbackrest/conf.d]\n" - " --repo-host-config-path pgBackRest repository host configuration\n" - " path [default=/etc/pgbackrest]\n" - " --repo-host-key-file repository host key file\n" - " --repo-host-port repository host port when repo-host is set\n" - " --repo-host-type repository host protocol type [default=ssh]\n" - " --repo-host-user repository host user when repo-host is set\n" - " [default=pgbackrest]\n" - " --repo-path path where backups and archive are stored\n" - " [default=/var/lib/pgbackrest]\n" - " --repo-s3-bucket S3 repository bucket\n" - " --repo-s3-endpoint S3 repository endpoint\n" - " --repo-s3-key S3 repository access key\n" - " --repo-s3-key-secret S3 repository secret access key\n" - " --repo-s3-key-type S3 repository key type [default=shared]\n" - " --repo-s3-kms-key-id S3 repository KMS key\n" - " --repo-s3-region S3 repository region\n" - " --repo-s3-role S3 repository role\n" - " --repo-s3-token S3 repository security token\n" - " --repo-s3-uri-style S3 URI Style [default=host]\n" - " --repo-storage-ca-file repository storage CA file\n" - " --repo-storage-ca-path repository storage CA path\n" - " --repo-storage-host repository storage host\n" - " --repo-storage-port repository storage port [default=443]\n" - " --repo-storage-upload-chunk-size repository storage upload chunk size\n" - " --repo-storage-verify-tls repository storage certificate verify\n" - " [default=y]\n" - " --repo-type type of storage used for the repository\n" - " [default=posix]\n" + " --repo set repository\n" + " --repo-azure-account azure repository account\n" + " --repo-azure-container azure repository container\n" + " --repo-azure-endpoint azure repository endpoint\n" + " [default=blob.core.windows.net]\n" + " --repo-azure-key azure repository key\n" + " --repo-azure-key-type azure repository key type [default=shared]\n" + " --repo-azure-uri-style azure URI Style [default=host]\n" + " --repo-cipher-pass repository cipher passphrase\n" + " [current=]\n" + " --repo-cipher-type cipher used to encrypt the repository\n" + " [current=aes-256-cbc, default=none]\n" + " --repo-gcs-bucket GCS repository bucket\n" + " --repo-gcs-endpoint GCS repository endpoint\n" + " [default=storage.googleapis.com]\n" + " --repo-gcs-key GCS repository key\n" + " --repo-gcs-key-type GCS repository key type [default=service]\n" + " --repo-host repository host when operating remotely\n" + " [current=backup.example.net]\n" + " --repo-host-ca-file repository host certificate authority file\n" + " --repo-host-ca-path repository host certificate authority path\n" + " --repo-host-cert-file repository host certificate file\n" + " --repo-host-cmd repository host pgBackRest command\n" + " [default=/path/to/pgbackrest]\n" + " --repo-host-config pgBackRest repository host configuration\n" + " file\n" + " [default=/etc/pgbackrest/pgbackrest.conf]\n" + " --repo-host-config-include-path pgBackRest repository host configuration\n" + " include path\n" + " [default=/etc/pgbackrest/conf.d]\n" + " --repo-host-config-path pgBackRest repository host configuration\n" + " path [default=/etc/pgbackrest]\n" + " --repo-host-key-file repository host key file\n" + " --repo-host-port repository host port when repo-host is set\n" + " --repo-host-type repository host protocol type\n" + " [default=ssh]\n" + " --repo-host-user repository host user when repo-host is\n" + " set [default=pgbackrest]\n" + " --repo-path path where backups and archive are stored\n" + " [default=/var/lib/pgbackrest]\n" + " --repo-s3-bucket S3 repository bucket\n" + " --repo-s3-endpoint S3 repository endpoint\n" + " --repo-s3-key S3 repository access key\n" + " --repo-s3-key-secret S3 repository secret access key\n" + " --repo-s3-key-type S3 repository key type [default=shared]\n" + " --repo-s3-kms-key-id S3 repository KMS key\n" + " --repo-s3-region S3 repository region\n" + " --repo-s3-role S3 repository role\n" + " --repo-s3-token S3 repository security token\n" + " --repo-s3-uri-style S3 URI Style [default=host]\n" + " --repo-sftp-host SFTP repository host\n" + " --repo-sftp-host-fingerprint SFTP repository host fingerprint\n" + " --repo-sftp-host-key-hash-type SFTP repository host key hash type\n" + " --repo-sftp-host-port SFTP repository host port [default=22]\n" + " --repo-sftp-host-user SFTP repository host user\n" + " --repo-sftp-private-key-file SFTP private key file\n" + " --repo-sftp-private-key-passphrase SFTP private key passphrase\n" + " --repo-sftp-public-key-file SFTP public key file\n" + " --repo-storage-ca-file repository storage CA file\n" + " --repo-storage-ca-path repository storage CA path\n" + " --repo-storage-host repository storage host\n" + " --repo-storage-port repository storage port [default=443]\n" + " --repo-storage-upload-chunk-size repository storage upload chunk size\n" + " --repo-storage-verify-tls repository storage certificate verify\n" + " [default=y]\n" + " --repo-type type of storage used for the repository\n" + " [default=posix]\n" "\n" "Stanza Options:\n" "\n" - " --pg-path postgreSQL data directory\n" + " --pg-path postgreSQL data directory\n" "\n" "Use 'pgbackrest help restore [option]' for more information.\n"); diff --git a/test/src/module/config/loadTest.c b/test/src/module/config/loadTest.c index 0ec4ec565..ef2c0de15 100644 --- a/test/src/module/config/loadTest.c +++ b/test/src/module/config/loadTest.c @@ -117,6 +117,25 @@ testRun(void) hrnCfgLoadP(cfgCmdInfo, argList), OptionInvalidValueError, "local repo1 and repo2 paths are both '/var/lib/pgbackrest' but must be different"); + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("local default repo paths for sftp repo type must be different"); + + argList = strLstNew(); + hrnCfgArgRawZ(argList, cfgOptRepo, "2"); + hrnCfgArgKeyRawStrId(argList, cfgOptRepoType, 1, STORAGE_SFTP_TYPE); + hrnCfgArgKeyRawStrId(argList, cfgOptRepoType, 2, STORAGE_SFTP_TYPE); + hrnCfgArgKeyRawZ(argList, cfgOptRepoSftpHostUser, 1, "user1"); + hrnCfgArgKeyRawZ(argList, cfgOptRepoSftpHostUser, 2, "user2"); + hrnCfgArgKeyRawZ(argList, cfgOptRepoSftpHost, 1, "host1"); + hrnCfgArgKeyRawZ(argList, cfgOptRepoSftpHost, 2, "host2"); + hrnCfgArgKeyRawZ(argList, cfgOptRepoSftpHostKeyHashType, 1, "sha1"); + hrnCfgArgKeyRawZ(argList, cfgOptRepoSftpHostKeyHashType, 2, "md5"); + hrnCfgArgKeyRawZ(argList, cfgOptRepoSftpPrivateKeyFile, 1, "/keyfile1"); + hrnCfgArgKeyRawZ(argList, cfgOptRepoSftpPrivateKeyFile, 2, "/keyfile2"); + TEST_ERROR( + hrnCfgLoadP(cfgCmdInfo, argList), OptionInvalidValueError, + "local repo1 and repo2 paths are both '/var/lib/pgbackrest' but must be different"); + // ------------------------------------------------------------------------------------------------------------------------- TEST_TITLE("local repo paths same but types different"); diff --git a/test/src/module/storage/sftpTest.c b/test/src/module/storage/sftpTest.c new file mode 100644 index 000000000..2ea79583d --- /dev/null +++ b/test/src/module/storage/sftpTest.c @@ -0,0 +1,4565 @@ +/*********************************************************************************************************************************** +Test SFTP Storage +***********************************************************************************************************************************/ +#include "common/io/io.h" +#include "common/io/session.h" +#include "common/io/socket/client.h" +#include "common/time.h" +#include "storage/cifs/storage.h" +#include "storage/read.h" +#include "storage/sftp/storage.h" +#include "storage/write.h" + +#include "common/harnessConfig.h" +#include "common/harnessFork.h" +#include "common/harnessLibSsh2.h" +#include "common/harnessSocket.h" +#include "common/harnessStorage.h" + +/*********************************************************************************************************************************** +Constants +***********************************************************************************************************************************/ +#define HANDSHAKE_PARAM "["STRINGIFY(HRN_SCK_FILE_DESCRIPTOR)"]" + +/*********************************************************************************************************************************** +Test function for path expression +***********************************************************************************************************************************/ +static String * +storageTestPathExpression(const String *expression, const String *path) +{ + String *result = NULL; + + if (strcmp(strZ(expression), "") == 0) + { + result = strCatZ(strNew(), "test"); + + if (path != NULL) + strCatFmt(result, "/%s", strZ(path)); + } + else if (strcmp(strZ(expression), "") != 0) + THROW_FMT(AssertError, "invalid expression '%s'", strZ(expression)); + + return result; +} + +/*********************************************************************************************************************************** +Test Run +***********************************************************************************************************************************/ +static void +testRun(void) +{ + FUNCTION_HARNESS_VOID(); + + // Install shim to return defined fd + hrnSckClientOpenShimInstall(); + + // Directory and file that cannot be accessed to test permissions errors + const String *fileNoPerm = STRDEF(TEST_PATH "/noperm/noperm"); + String *pathNoPerm = strPath(fileNoPerm); + + // Write file for testing if storage is read-only + const String *writeFile = STRDEF(TEST_PATH "/writefile"); + + // This test should always be first so the storage helper is uninitialized + // ***************************************************************************************************************************** + if (testBegin("storageHelperDryRunInit()")) + { + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("writable storage fails when dry-run is not initialized"); + + TEST_ERROR(storagePgIdxWrite(0), AssertError, WRITABLE_WHILE_DRYRUN); + TEST_ERROR(storageRepoIdxWrite(0), AssertError, WRITABLE_WHILE_DRYRUN); + TEST_ERROR(storageSpoolWrite(), AssertError, WRITABLE_WHILE_DRYRUN); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("writable storage fails when dry-run is true"); + + storageHelperDryRunInit(true); + + TEST_ERROR(storagePgIdxWrite(0), AssertError, WRITABLE_WHILE_DRYRUN); + TEST_ERROR(storageRepoIdxWrite(0), AssertError, WRITABLE_WHILE_DRYRUN); + TEST_ERROR(storageSpoolWrite(), AssertError, WRITABLE_WHILE_DRYRUN); + } + + // ***************************************************************************************************************************** + if (testBegin("storageNew()")) + { + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("ssh2 init failure"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = -1}, + {.function = NULL} + }); + + TEST_ERROR( + storageSftpNewP(TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 5, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB), + ServiceError, "unable to init libssh2"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("driver session failure"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]", .resultNull = true}, + {.function = NULL} + }); + + TEST_ERROR( + storageSftpNewP(TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 5, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB), + ServiceError, "unable to init libssh2 session"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("handshake failure"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = -1}, + {.function = NULL} + }); + + TEST_ERROR( + storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 1000, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB), + ServiceError, "libssh2 handshake failed [-1]"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("handshake failure"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 100}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = NULL} + }); + + TEST_ERROR( + storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 100, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB), + ServiceError, "libssh2 handshake failed [-37]"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("hostkey hash fail"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultNull = true}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_PROTO}, + {.function = NULL} + }); + + TEST_ERROR( + storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 1000, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB), + ServiceError, "libssh2 hostkey hash failed: libssh2 errno [-14]"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("invalid hostkey hash"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = 0}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = 0}, + {.function = NULL} + }); + + TEST_ERROR( + storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, cipherTypeAes256Cbc, .keyPub = KEYPUB, + .write = true), + ServiceError, "requested ssh2 hostkey hash type (aes-256-cbc) not available"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("public key from file auth failure"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = "12345678909876543210"}, + {.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX, + .param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_CSTR "\",\"" KEYPRIV_CSTR "\",null]", .resultInt = -16}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_OK}, + {.function = NULL} + }); + + TEST_ERROR( + storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 1000, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, + .hostFingerprint = STRDEF("3132333435363738393039383736353433323130")), + ServiceError, + "public key authentication failed: libssh2 error [-16]\n" + "HINT: libssh2 compiled against non-openssl libraries requires --repo-sftp-private-key-file and" + " --repo-sftp-public-key-file to be provided\n" + "HINT: libssh2 versions before 1.9.0 expect a PEM format keypair, try ssh-keygen -m PEM -t rsa -P \"\" to generate the" + " keypair"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("fingerprint mismatch"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = "12345678909876543210"}, + {.function = NULL} + }); + + TEST_ERROR( + storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 1000, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, + .hostFingerprint = STRDEF("9132333435363738393039383736353433323130")), + ServiceError, + "host [3132333435363738393039383736353433323130] and configured fingerprint (repo-sftp-host-fingerprint)" + " [9132333435363738393039383736353433323130] do not match"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("public key from file auth failure, no public key"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = "01234567899876543210"}, + {.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX, + .param = "[\"" TEST_USER "\"," TEST_USER_LEN ",null,\"" KEYPRIV_CSTR "\",null]", .resultInt = -16}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_OK}, + {.function = NULL} + }); + + TEST_ERROR( + storageSftpNewP(TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 1000, KEYPRIV, hashTypeSha1), ServiceError, + "public key authentication failed: libssh2 error [-16]\n" + "HINT: libssh2 compiled against non-openssl libraries requires --repo-sftp-private-key-file and" + " --repo-sftp-public-key-file to be provided\n" + "HINT: libssh2 versions before 1.9.0 expect a PEM format keypair, try ssh-keygen -m PEM -t rsa -P \"\" to generate the" + " keypair"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("public key from file auth failure, key passphrase"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = "01234567899876543210"}, + {.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX, + .param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_CSTR "\",\"" KEYPRIV_CSTR "\",\"keyPassphrase\"]", + .resultInt = -16}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_OK}, + {.function = NULL} + }); + + TEST_ERROR( + storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 1000, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, + .keyPassphrase = STRDEF("keyPassphrase")), + ServiceError, + "public key authentication failed: libssh2 error [-16]\n" + "HINT: libssh2 compiled against non-openssl libraries requires --repo-sftp-private-key-file and" + " --repo-sftp-public-key-file to be provided\n" + "HINT: libssh2 versions before 1.9.0 expect a PEM format keypair, try ssh-keygen -m PEM -t rsa -P \"\" to generate the" + " keypair"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("public key from file LIBSSH2_ERROR_EAGAIN, failure"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = "12345678910123456789"}, + {.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX, + .param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_CSTR "\",\"" KEYPRIV_CSTR "\",null]", + .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 100}, + {.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX, + .param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_CSTR "\",\"" KEYPRIV_CSTR "\",null]", + .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_OK}, + {.function = NULL} + }); + + TEST_ERROR( + storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 100, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB), ServiceError, + "public key authentication failed: libssh2 error [-37]\n" + "HINT: libssh2 compiled against non-openssl libraries requires --repo-sftp-private-key-file and" + " --repo-sftp-public-key-file to be provided\n" + "HINT: libssh2 versions before 1.9.0 expect a PEM format keypair, try ssh-keygen -m PEM -t rsa -P \"\" to generate the" + " keypair"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("sftp session init failure"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = "12345678910123456789"}, + {.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX, + .param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_CSTR "\",\"" KEYPRIV_CSTR "\",null]", + .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_INIT, .resultNull = true}, + {.function = NULL} + }); + + TEST_ERROR( + storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 1, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB), ServiceError, + "unable to init libssh2_sftp session"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("sftp session init fail && timeout"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = "12345678910123456789"}, + {.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX, + .param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_CSTR "\",\"" KEYPRIV_CSTR "\",null]", + .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_INIT, .resultNull = true, .sleep = 100}, + {.function = HRNLIBSSH2_SFTP_INIT, .resultNull = true, .sleep = 20}, + {.function = NULL} + }); + + TEST_ERROR( + storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 100, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB), ServiceError, + "unable to init libssh2_sftp session"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("sftp session init success"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + Storage *storageTest = NULL; + + TEST_ASSIGN( + storageTest, + storageSftpNewP(STRDEF("/tmp"), STRDEF("localhost"), 22, TEST_USER_STR, 5, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB), + "new storage (defaults)"); + TEST_RESULT_BOOL(storageTest->write, false, "check write"); + + // Free context, otherwise callbacks to storageSftpLibSsh2SessionFreeResource() accumulate + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("sftp session init success timeout"); + + // Shim sets FD for tests. + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = "12345678910123456789"}, + {.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX, + .param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_CSTR "\",\"" KEYPRIV_CSTR "\",null]", + .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_INIT, .resultInt = LIBSSH2_ERROR_NONE, .sleep = 100}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + TEST_ASSIGN( + storageTest, + storageSftpNewP( + STRDEF("/tmp"), STRDEF("localhost"), 22, TEST_USER_STR, 100, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB), + "new storage (defaults)"); + TEST_RESULT_BOOL(storageTest->write, false, "check write"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("create new storage with defaults"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + TEST_ASSIGN( + storageTest, + storageSftpNewP(STRDEF("/tmp"), STRDEF("localhost"), 22, TEST_USER_STR, 1000, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB), + "new storage (defaults)"); + TEST_RESULT_STR_Z(storageTest->path, "/tmp", "check path"); + TEST_RESULT_INT(storageTest->modeFile, 0640, "check file mode"); + TEST_RESULT_INT(storageTest->modePath, 0750, "check path mode"); + TEST_RESULT_BOOL(storageTest->write, false, "check write"); + TEST_RESULT_BOOL(storageTest->pathExpressionFunction == NULL, true, "check expression function is not set"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("create new storage - override defaults"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + TEST_ASSIGN( + storageTest, + storageSftpNewP( + STRDEF("/path/to"), STRDEF("localhost"), 22, TEST_USER_STR, 1000, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, + .modeFile = 0600, .modePath = 0700, .write = true), + "new storage (non-default)"); + TEST_RESULT_STR_Z(storageTest->path, "/path/to", "check path"); + TEST_RESULT_INT(storageTest->modeFile, 0600, "check file mode"); + TEST_RESULT_INT(storageTest->modePath, 0700, "check path mode"); + TEST_RESULT_BOOL(storageTest->write, true, "check write"); + TEST_RESULT_BOOL(storageTest->pathExpressionFunction == NULL, true, "check expression function is empty"); + + TEST_RESULT_PTR(storageInterface(storageTest).info, storageTest->pub.interface.info, "check interface"); + TEST_RESULT_PTR(storageDriver(storageTest), storageTest->pub.driver, "check driver"); + TEST_RESULT_UINT(storageType(storageTest), storageTest->pub.type, "check type"); + TEST_RESULT_BOOL(storageFeature(storageTest, storageFeaturePath), true, "check path feature"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + } + + // ***************************************************************************************************************************** + if (testBegin("storageExists() and storagePathExists()")) + { + // Mimic creation of /noperm/noperm for permission denied + // drwx------ 2 root root 3 Dec 5 15:30 noperm + // noperm/: + // total 1 + // -rw------- 1 root root 0 Dec 5 15:30 noperm + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + // File missing + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/missing\",0]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/missing\",0]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE, .sleep = 100}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/missing\",0]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + // Path missing + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/missing\",0]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "\",0]", + .attrPerms = LIBSSH2_SFTP_S_IFDIR, .resultInt = LIBSSH2_ERROR_NONE}, + // Permission denied + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/noperm/noperm\",0]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/noperm/noperm\",0]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + // File and path + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/exists\",0]", + .attrPerms = LIBSSH2_SFTP_S_IFREG, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pathExists\",0]", + .attrPerms = LIBSSH2_SFTP_S_IFDIR, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/exists\",0]", + .attrPerms = LIBSSH2_SFTP_S_IFREG, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pathExists\",0]", + .attrPerms = LIBSSH2_SFTP_S_IFDIR, .resultInt = LIBSSH2_ERROR_NONE}, + // File exists after wait + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/exists\",0]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL, .sleep = 25}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/exists\",0]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL, .sleep = 25}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/exists\",0]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL, .sleep = 25}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/exists\",0]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL, .sleep = 25}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/exists\",0]", + .attrPerms = LIBSSH2_SFTP_S_IFREG, .resultInt = LIBSSH2_ERROR_NONE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + Storage *storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 200, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("file"); + + TEST_RESULT_BOOL(storageExistsP(storageTest, STRDEF("missing")), false, "file does not exist"); + TEST_RESULT_BOOL(storageExistsP(storageTest, STRDEF("missing"), .timeout = 100), false, "file does not exist"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("path"); + + TEST_RESULT_BOOL(storagePathExistsP(storageTest, STRDEF("missing")), false, "path does not exist"); + TEST_RESULT_BOOL(storagePathExistsP(storageTest, NULL), true, "test path exists"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("permission denied"); + + TEST_ERROR_FMT(storageExistsP(storageTest, fileNoPerm), FileOpenError, STORAGE_ERROR_INFO, strZ(fileNoPerm)); + TEST_ERROR_FMT(storagePathExistsP(storageTest, fileNoPerm), FileOpenError, STORAGE_ERROR_INFO, strZ(fileNoPerm)); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("file and path"); + + const String *fileExists = STRDEF(TEST_PATH "/exists"); + const String *pathExists = STRDEF(TEST_PATH "/pathExists"); + + TEST_RESULT_BOOL(storageExistsP(storageTest, fileExists), true, "file exists"); + TEST_RESULT_BOOL(storageExistsP(storageTest, pathExists), false, "not a file"); + TEST_RESULT_BOOL(storagePathExistsP(storageTest, fileExists), false, "not a path"); + TEST_RESULT_BOOL(storagePathExistsP(storageTest, pathExists), true, "path exists"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("file after wait"); + + HRN_FORK_BEGIN() + { + HRN_FORK_CHILD_BEGIN() + { + sleepMSec(250); + + // Mimic HRN_SYSTEM_FMT("touch %s", strZ(fileExists)); + } + HRN_FORK_CHILD_END(); + + HRN_FORK_PARENT_BEGIN() + { + TEST_RESULT_BOOL(storageExistsP(storageTest, fileExists, .timeout = 1000), true, "file exists after wait"); + } + HRN_FORK_PARENT_END(); + } + HRN_FORK_END(); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + } + + // ***************************************************************************************************************************** + if (testBegin("storageInfo()")) + { + // File open error via permission denied + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/noperm/noperm\",1]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + // Create storage object for testing + Storage *storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 4000, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ERROR_FMT(storageInfoP(storageTest, fileNoPerm), FileOpenError, STORAGE_ERROR_INFO, strZ(fileNoPerm)); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("info for / exists"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"/\",1]", .resultInt = LIBSSH2_ERROR_NONE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + // Create storage object for testing + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 1000, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB); + + TEST_RESULT_BOOL(storageInfoP(storageTest, NULL).exists, true, "info for /"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("info for / does not exist with no path feature"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + Storage *storageRootNoPath = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 1000, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB); + storageRootNoPath->pub.interface.feature ^= 1 << storageFeaturePath; + + TEST_RESULT_BOOL(storageInfoP(storageRootNoPath, NULL, .ignoreMissing = true).exists, false, "no info for /"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageRootNoPath))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("directory does not exist"); + + const String *fileName = STRDEF(TEST_PATH "/fileinfo"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + // File does not exist + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/fileinfo\",1]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/fileinfo\",1]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + // Info out of base path - first test libssh2_sftp_stat_ex throws error before reaching libssh2 code second test + // libssh2_sftp_stat_stat_ex reaches libssh2 code + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"/etc\",1]", .resultInt = LIBSSH2_ERROR_NONE}, + // Info - path + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "\",1]", + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // Info - path exists only + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "\",1]", + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // Info basic - path + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "\",1]", + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // Info - file + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/fileinfo\",1]", + .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID | + LIBSSH2_SFTP_ATTR_SIZE, + .mtime = 1555155555, .uid = 99999, .gid = 99999, .filesize = 8}, + // Info - link + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/testlink\",1]", + .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFLNK | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG | LIBSSH2_SFTP_S_IRWXO, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_SYMLINK_EX, .param = "[\"" TEST_PATH "/testlink\",\"\",1]", + .resultInt = LIBSSH2_ERROR_NONE, .symlinkExTarget = STRDEF("/tmp")}, + // Info - link .followLink = true + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/testlink\",0]", .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/testlink\",0]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG | LIBSSH2_SFTP_S_IRWXO, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .uid = 0, .gid = 0}, + // Info - link .followLink = true, timeout EAGAIN in followLink + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/testlink\",0]", .resultInt = LIBSSH2_ERROR_EAGAIN, + .sleep = 23}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/testlink\",0]", .resultInt = LIBSSH2_ERROR_EAGAIN, + .sleep = 1}, + // Info - link .followLink = false, timeout EAGAIN + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/testlink\",1]", .resultInt = LIBSSH2_ERROR_EAGAIN, + .sleep = 23}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/testlink\",1]", .resultInt = LIBSSH2_ERROR_EAGAIN, + .sleep = 1}, + // Info - link .followLink = false, libssh2_sftp_symlink_ex link destination size timeout + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/testlink\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFLNK | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG | LIBSSH2_SFTP_S_IRWXO, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_SYMLINK_EX, .param = "[\"" TEST_PATH "/testlink\",\"\",1]", + .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 18}, + {.function = HRNLIBSSH2_SFTP_SYMLINK_EX, .param = "[\"" TEST_PATH "/testlink\",\"\",1]", + .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 5}, + // Info - pipe + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/testpipe\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFIFO | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | + LIBSSH2_SFTP_S_IWGRP | LIBSSH2_SFTP_S_IROTH | LIBSSH2_SFTP_S_IWOTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + // Create storage object for testing + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ERROR_FMT(storageInfoP(storageTest, fileName), FileOpenError, STORAGE_ERROR_INFO_MISSING, strZ(fileName)); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("file does not exist"); + + StorageInfo info = {0}; + TEST_ASSIGN(info, storageInfoP(storageTest, fileName, .ignoreMissing = true), "get file info (missing)"); + TEST_RESULT_BOOL(info.exists, false, "check not exists"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("info outside of base path"); + + TEST_ERROR( + storageInfoP(storageTest, STRDEF("/etc"), .ignoreMissing = true), AssertError, + "absolute path '/etc' is not in base path '" TEST_PATH "'"); + TEST_RESULT_BOOL( + storageInfoP(storageTest, STRDEF("/etc"), .ignoreMissing = true, .noPathEnforce = true).exists, true, + "path not enforced"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("info - path"); + + HRN_STORAGE_TIME(storageTest, TEST_PATH, 1555160000); + + TEST_ASSIGN(info, storageInfoP(storageTest, TEST_PATH_STR), "get path info"); + TEST_RESULT_STR(info.name, NULL, "name is not set"); + TEST_RESULT_BOOL(info.exists, true, "check exists"); + TEST_RESULT_INT(info.type, storageTypePath, "check type"); + TEST_RESULT_UINT(info.size, 0, "check size"); + TEST_RESULT_INT(info.mode, 0770, "check mode"); + TEST_RESULT_INT(info.timeModified, 1555160000, "check mod time"); + TEST_RESULT_STR(info.linkDestination, NULL, "no link destination"); + TEST_RESULT_UINT(info.userId, TEST_USER_ID, "check user id"); + TEST_RESULT_STR(info.user, NULL, "check user"); + TEST_RESULT_UINT(info.groupId, TEST_GROUP_ID, "check group id"); + TEST_RESULT_STR(info.group, NULL, "check group"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("info - path existence only"); + + TEST_ASSIGN(info, storageInfoP(storageTest, TEST_PATH_STR, .level = storageInfoLevelExists), "path exists"); + TEST_RESULT_BOOL(info.exists, true, "check exists"); + TEST_RESULT_UINT(info.size, 0, "check exists"); + TEST_RESULT_INT(info.timeModified, 0, "check time"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("info basic - path"); + + storageTest->pub.interface.feature ^= 1 << storageFeatureInfoDetail; + + TEST_ASSIGN(info, storageInfoP(storageTest, TEST_PATH_STR), "get path info"); + TEST_RESULT_STR(info.name, NULL, "name is not set"); + TEST_RESULT_BOOL(info.exists, true, "check exists"); + TEST_RESULT_INT(info.type, storageTypePath, "check type"); + TEST_RESULT_UINT(info.size, 0, "check size"); + TEST_RESULT_INT(info.mode, 0, "check mode"); + TEST_RESULT_INT(info.timeModified, 1555160000, "check mod time"); + TEST_RESULT_STR(info.linkDestination, NULL, "no link destination"); + TEST_RESULT_UINT(info.userId, 0, "check user id"); + TEST_RESULT_STR(info.user, NULL, "check user"); + TEST_RESULT_UINT(info.groupId, 0, "check group id"); + TEST_RESULT_STR(info.group, NULL, "check group"); + + storageTest->pub.interface.feature ^= 1 << storageFeatureInfoDetail; + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("info - file"); + + // Mimic file chown'd 99999:99999, timestamp 1555155555, containing "TESTFILE" + TEST_ASSIGN(info, storageInfoP(storageTest, fileName), "get file info"); + TEST_RESULT_STR(info.name, NULL, "name is not set"); + TEST_RESULT_BOOL(info.exists, true, "check exists"); + TEST_RESULT_INT(info.type, storageTypeFile, "check type"); + TEST_RESULT_UINT(info.size, 8, "check size"); + TEST_RESULT_INT(info.mode, 0640, "check mode"); + TEST_RESULT_INT(info.timeModified, 1555155555, "check mod time"); + TEST_RESULT_STR(info.linkDestination, NULL, "no link destination"); + TEST_RESULT_STR(info.user, NULL, "check user"); + TEST_RESULT_STR(info.group, NULL, "check group"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("info - link"); + + const String *linkName = STRDEF(TEST_PATH "/testlink"); + + // Mimic link testlink to /tmp + TEST_ASSIGN(info, storageInfoP(storageTest, linkName), "get link info"); + TEST_RESULT_STR(info.name, NULL, "name is not set"); + TEST_RESULT_BOOL(info.exists, true, "check exists"); + TEST_RESULT_INT(info.type, storageTypeLink, "check type"); + TEST_RESULT_UINT(info.size, 0, "check size"); + TEST_RESULT_INT(info.mode, 0777, "check mode"); + TEST_RESULT_STR_Z(info.linkDestination, "/tmp", "check link destination"); + TEST_RESULT_STR(info.user, NULL, "check user"); + TEST_RESULT_STR(info.group, NULL, "check group"); + + TEST_ASSIGN(info, storageInfoP(storageTest, linkName, .followLink = true), "get info from path pointed to by link"); + TEST_RESULT_STR(info.name, NULL, "name is not set"); + TEST_RESULT_BOOL(info.exists, true, "check exists"); + TEST_RESULT_INT(info.type, storageTypePath, "check type"); + TEST_RESULT_UINT(info.size, 0, "check size"); + TEST_RESULT_INT(info.mode, 0777, "check mode"); + TEST_RESULT_STR(info.linkDestination, NULL, "check link destination"); + TEST_RESULT_STR_Z(info.user, NULL, "check user"); + TEST_RESULT_STR_Z(info.group, NULL, "check group"); + + // Exercise paths/branches + // libssh2_sftp_stat_ex timeout EAGAIN followLink true + TEST_ERROR_FMT( + storageInfoP(storageTest, linkName, .followLink = true), FileOpenError, + "unable to get info for path/file '" TEST_PATH "/testlink'"); + + // libssh2_sftp_stat_ex timeout EAGAIN followLink false + TEST_ERROR_FMT( + storageInfoP(storageTest, linkName, .followLink = false), FileOpenError, + "unable to get info for path/file '" TEST_PATH "/testlink'"); + + // libssh2_sftp_symlink_ex link destination timeout EAGAIN followLink false + TEST_ERROR_FMT( + storageInfoP(storageTest, linkName, .followLink = false), FileReadError, + "unable to get destination for link '" TEST_PATH "/testlink'"); + + // -------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("info - pipe"); + + const String *pipeName = STRDEF(TEST_PATH "/testpipe"); + + // Mimic pipe "/testpipe" with mode 0666 + TEST_ASSIGN(info, storageInfoP(storageTest, pipeName), "get info from pipe (special file)"); + TEST_RESULT_STR(info.name, NULL, "name is not set"); + TEST_RESULT_BOOL(info.exists, true, "check exists"); + TEST_RESULT_INT(info.type, storageTypeSpecial, "check type"); + TEST_RESULT_UINT(info.size, 0, "check size"); + TEST_RESULT_INT(info.mode, 0666, "check mode"); + TEST_RESULT_STR(info.linkDestination, NULL, "check link destination"); + TEST_RESULT_STR(info.user, NULL, "check user"); + TEST_RESULT_STR(info.group, NULL, "check group"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + } + + // ***************************************************************************************************************************** + if (testBegin("storageNewItrP()")) + { + // Mimic creation of /noperm/noperm + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + // Path missing errorOnMissing true + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/BOGUS\",0,0,1]", .resultNull = true}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/BOGUS\",0,0,1]", .resultNull = true}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + // Ignore missing dir + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/BOGUS\",0,0,1]", .resultNull = true}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + // Path no perm + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/noperm\",0,0,1]", .resultNull = true}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + // Path no perm ignore missing + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/noperm\",0,0,1]", .resultNull = true}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + // Helper function storageSftpListEntry() + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"pg/missing\",1]", .resultNull = true}, + // Path with only dot + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg\",0]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH | LIBSSH2_SFTP_S_IWOTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg\",0,0,1]"}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("."), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH | LIBSSH2_SFTP_S_IWOTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // Path with file, link, pipe, dot dotdot + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg\",0]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH | LIBSSH2_SFTP_S_IWOTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg\",0,0,1]"}, + // readdir returns . + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("."), + .resultInt = 3, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir returns .. + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\".\",4095,null,0]", .fileName = STRDEF(".."), + .resultInt = 8, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir returns .include + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"..\",4095,null,0]", .fileName = STRDEF(".include"), + .resultInt = 8, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = 77777, .gid = 77777}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/.include\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IXGRP | + LIBSSH2_SFTP_S_IROTH | LIBSSH2_SFTP_S_IXOTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = 77777, .gid = 77777}, + // readdir returns file + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\".include\",4095,null,0]", .fileName = STRDEF("file"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/file\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | + LIBSSH2_SFTP_S_IWGRP, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID | + LIBSSH2_SFTP_ATTR_SIZE, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .filesize = 8}, + // readdir returns link + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"file\",4095,null,0]", .fileName = STRDEF("link"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFLNK | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/link\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFLNK | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .symlinkExTarget = STRDEF("../file")}, + {.function = HRNLIBSSH2_SFTP_SYMLINK_EX, .param = "[\"" TEST_PATH "/pg/link\",\"\",1]", + .resultInt = LIBSSH2_ERROR_NONE, .symlinkExTarget = STRDEF("../file")}, + // readdir returns pipe + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"link\",4095,null,0]", .fileName = STRDEF("pipe"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFIFO | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/pipe\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFIFO | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir returns empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"pipe\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFIFO | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg/.include\",0,0,1]"}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IXGRP | + LIBSSH2_SFTP_S_IROTH | LIBSSH2_SFTP_S_IXOTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = 77777, .gid = 77777}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH | LIBSSH2_SFTP_S_IWOTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/link\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFLNK | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .symlinkExTarget = STRDEF("../file")}, + {.function = HRNLIBSSH2_SFTP_SYMLINK_EX, .param = "[\"" TEST_PATH "/pg/link\",\"\",1]", + .resultInt = LIBSSH2_ERROR_NONE, .symlinkExTarget = STRDEF("../file")}, + // Helper function - storageSftpListEntry() info doesn't exist + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"pg/missing\",1]", .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"pg/missing\",1]", .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + // Create storage object for testing + Storage *storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("path missing"); + + TEST_ERROR_FMT( + storageNewItrP(storageTest, STRDEF(BOGUS_STR), .errorOnMissing = true), PathMissingError, + STORAGE_ERROR_LIST_INFO_MISSING, TEST_PATH "/BOGUS"); + + TEST_RESULT_PTR(storageNewItrP(storageTest, STRDEF(BOGUS_STR), .nullOnMissing = true), NULL, "ignore missing dir"); + + TEST_ERROR_FMT( + storageNewItrP(storageTest, pathNoPerm, .errorOnMissing = true), PathOpenError, + STORAGE_ERROR_LIST_INFO ": libssh2 error [-31]: sftp error [3]", strZ(pathNoPerm)); + + // Should still error even when ignore missing + TEST_ERROR_FMT( + storageNewItrP(storageTest, pathNoPerm), PathOpenError, + STORAGE_ERROR_LIST_INFO ": libssh2 error [-31]: sftp error [3]", strZ(pathNoPerm)); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("helper function - storageSftpListEntry()"); + + TEST_RESULT_VOID( + storageSftpListEntry( + (StorageSftp *)storageDriver(storageTest), storageLstNew(storageInfoLevelBasic), STRDEF("pg"), "missing", + storageInfoLevelBasic), + "missing path"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("path with only dot"); + + // Mimic creation of TEST_PATH "/pg" mode 0766 + + // NOTE: if operating against an actual sftp server, a neutral umask is required to get the proper permissions. + // Without the neutral umask, permissions were 0764. + TEST_STORAGE_LIST( + storageTest, "pg", + "./ {u=" TEST_USER_ID_Z ", g=" TEST_GROUP_ID_Z ", m=0766}\n", + .level = storageInfoLevelDetail, .includeDot = true); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("path with file, link, pipe"); + + // Mimic creation of TEST_PATH "/pg/.include" mode 0755 chown'd 77777:77777 + // Mimic creation of TEST_PATH "pg/file" mode 0660 timemodified 1656433838 containing "TESTDATA" + // Mimic creation of TEST_PATH "/pg/link" linked to "../file" + // Mimic creation of TEST_PATH "/pg/pipe" mode 777 + TEST_STORAGE_LIST( + storageTest, "pg", + "./ {u=" TEST_USER_ID_Z ", g=" TEST_GROUP_ID_Z ", m=0766}\n" + ".include/ {u=77777, g=77777, m=0755}\n" + "file {s=8, t=1656433838, u=" TEST_USER_ID_Z ", g=" TEST_GROUP_ID_Z ", m=0660}\n" + "link> {d=../file, u=" TEST_USER_ID_Z ", g=" TEST_GROUP_ID_Z "}\n" + "pipe*\n", + .level = storageInfoLevelDetail, .includeDot = true); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("helper function - storageSftpListEntry() info doesn't exist"); + + TEST_RESULT_VOID( + storageSftpListEntry( + (StorageSftp *)storageDriver(storageTest), storageLstNew(storageInfoLevelBasic), STRDEF("pg"), "missing", + storageInfoLevelBasic), + "missing path"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageItrMore() twice in a row"); + + StorageIterator *storageItr = NULL; + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg\",0,0,1]"}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("."), + .resultInt = 3, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\".\",4095,null,0]", .fileName = STRDEF(".include"), + .resultInt = 8, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = 77777, .gid = 77777}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/.include\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IXGRP | + LIBSSH2_SFTP_S_IROTH | LIBSSH2_SFTP_S_IXOTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = 77777, .gid = 77777}, + // readdir returns file + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\".include\",4095,null,0]", .fileName = STRDEF("file"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/file\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | + LIBSSH2_SFTP_S_IWGRP, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID | + LIBSSH2_SFTP_ATTR_SIZE, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .filesize = 8}, + // readdir returns link + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"file\",4095,null,0]", .fileName = STRDEF("link"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFLNK | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/link\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFLNK | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .symlinkExTarget = STRDEF("../file")}, + {.function = HRNLIBSSH2_SFTP_SYMLINK_EX, .param = "[\"" TEST_PATH "/pg/link\",\"\",1]", + .resultInt = LIBSSH2_ERROR_NONE, .symlinkExTarget = STRDEF("../file")}, + // readdir returns pipe + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"link\",4095,null,0]", .fileName = STRDEF("pipe"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFIFO | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/pipe\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFIFO | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir returns empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"pipe\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFIFO | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + // Create storage object for testing + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 4000, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB); + + TEST_ASSIGN(storageItr, storageNewItrP(storageTest, STRDEF("pg")), "new iterator"); + TEST_RESULT_BOOL(storageItrMore(storageItr), true, "check more"); + TEST_RESULT_BOOL(storageItrMore(storageItr), true, "check more again"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("path - recurse desc"); + + // Mimic creation of "pg/path" mode 0700 + // Mimic creation of "pg/path/file" mode 0600 timemodified 1656434296 containing "TESTDATA" + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg\",0]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg\",0,0,1]"}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("."), + .resultInt = 3, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\".\",4095,null,0]", .fileName = STRDEF("path"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = 77777, .gid = 77777}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/path\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IXGRP | + LIBSSH2_SFTP_S_IROTH | LIBSSH2_SFTP_S_IXOTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = 77777, .gid = 77777}, + // readdir returns file + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"path\",4095,null,0]", .fileName = STRDEF("file"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/file\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | + LIBSSH2_SFTP_S_IWGRP, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID | + LIBSSH2_SFTP_ATTR_SIZE, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .filesize = 8}, + // readdir returns link + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"file\",4095,null,0]", .fileName = STRDEF("link"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFLNK | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/link\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFLNK | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .symlinkExTarget = STRDEF("../file")}, + // readdir returns pipe + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"link\",4095,null,0]", .fileName = STRDEF("pipe"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFIFO | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/pipe\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFIFO | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir returns empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"pipe\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFIFO | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + // Recurse readdir path + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg/path\",0,0,1]"}, + // readdir returns file + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("file"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/path/file\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | + LIBSSH2_SFTP_S_IWGRP, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID | + LIBSSH2_SFTP_ATTR_SIZE, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .filesize = 8}, + // readdir returns empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"file\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/link\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFLNK | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .symlinkExTarget = STRDEF("../file")}, + {.function = HRNLIBSSH2_SFTP_SYMLINK_EX, .param = "[\"" TEST_PATH "/pg/link\",\"\",1]", + .resultInt = LIBSSH2_ERROR_NONE, .symlinkExTarget = STRDEF("../file")}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + // Create storage object for testing + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 4000, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB); + + TEST_STORAGE_LIST( + storageTest, "pg", + "pipe*\n" + "path/file {s=8, t=1656434296}\n" + "path/\n" + "link> {d=../file}\n" + "file {s=8, t=1656433838}\n" + "./\n", + .level = storageInfoLevelBasic, .includeDot = true, .sortOrder = sortOrderDesc); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("path - recurse asc"); + + // Create a path with a subpath that will always be last to make sure lists are not freed too early in the iterator + // Mimic creation of "pg/zzz/yyy" mode 0700 + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg\",0]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg\",0,0,1]"}, + // readdir returns dot + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("."), + .resultInt = 3, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir returns path + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\".\",4095,null,0]", .fileName = STRDEF("path"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = 77777, .gid = 77777}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/path\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IXGRP | + LIBSSH2_SFTP_S_IROTH | LIBSSH2_SFTP_S_IXOTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = 77777, .gid = 77777}, + // readdir returns file + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"path\",4095,null,0]", .fileName = STRDEF("file"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/file\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | + LIBSSH2_SFTP_S_IWGRP, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID | + LIBSSH2_SFTP_ATTR_SIZE, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .filesize = 8}, + // readdir returns link + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"file\",4095,null,0]", .fileName = STRDEF("link"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFLNK | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/link\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFLNK | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .symlinkExTarget = STRDEF("../file")}, + // readdir returns pipe + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"link\",4095,null,0]", .fileName = STRDEF("pipe"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFIFO | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/pipe\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFIFO | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir returns zzz + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"pipe\",4095,null,0]", .fileName = STRDEF("zzz"), + .resultInt = 3, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/zzz\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir return empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"zzz\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + // Recurse readdir path + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg/path\",0,0,1]"}, + // readdir returns file + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("file"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/path/file\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | + LIBSSH2_SFTP_S_IWGRP, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID | + LIBSSH2_SFTP_ATTR_SIZE, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .filesize = 8}, + // readdir returns empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"file\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + // Recurse zzz + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg/zzz\",0,0,1]"}, + // readdir returns yyy + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("yyy"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/zzz/yyy\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir returns empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"yyy\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + // Recurse yyy + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg/zzz/yyy\",0,0,1]"}, + // readdir returns empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/link\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFLNK | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .symlinkExTarget = STRDEF("../file")}, + {.function = HRNLIBSSH2_SFTP_SYMLINK_EX, .param = "[\"" TEST_PATH "/pg/link\",\"\",1]", + .resultInt = LIBSSH2_ERROR_NONE, .symlinkExTarget = STRDEF("../file")}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + // Create storage object for testing + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 4000, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB); + + TEST_STORAGE_LIST( + storageTest, "pg", + "./\n" + "file {s=8, t=1656433838}\n" + "link> {d=../file}\n" + "path/\n" + "path/file {s=8, t=1656434296}\n" + "pipe*\n" + "zzz/\n" + "zzz/yyy/\n", + .level = storageInfoLevelBasic, .includeDot = true); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("path basic info - recurse"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg\",0]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg\",0,0,1]"}, + // readdir returns dot + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("."), + .resultInt = 3, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir returns path + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\".\",4095,null,0]", .fileName = STRDEF("path"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = 77777, .gid = 77777}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/path\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IXGRP | + LIBSSH2_SFTP_S_IROTH | LIBSSH2_SFTP_S_IXOTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = 77777, .gid = 77777}, + // readdir returns file + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"path\",4095,null,0]", .fileName = STRDEF("file"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/file\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | + LIBSSH2_SFTP_S_IWGRP, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID | + LIBSSH2_SFTP_ATTR_SIZE, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .filesize = 8}, + // readdir returns link + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"file\",4095,null,0]", .fileName = STRDEF("link"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFLNK | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/link\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFLNK | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .symlinkExTarget = STRDEF("../file")}, + // readdir returns pipe + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"link\",4095,null,0]", .fileName = STRDEF("pipe"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFIFO | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/pipe\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFIFO | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir returns zzz + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"pipe\",4095,null,0]", .fileName = STRDEF("zzz"), + .resultInt = 3, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/zzz\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir return empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"zzz\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + // Recurse zzz + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg/zzz\",0,0,1]"}, + // readdir returns yyy + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("yyy"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/zzz/yyy\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir returns empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"yyy\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + // Recurse yyy + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg/zzz/yyy\",0,0,1]"}, + // readdir returns empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + // Recurse path + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg/path\",0,0,1]"}, + // readdir returns file + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("file"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/path/file\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | + LIBSSH2_SFTP_S_IWGRP, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID | + LIBSSH2_SFTP_ATTR_SIZE, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .filesize = 8}, + // readdir returns empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"file\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/link\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFLNK | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .symlinkExTarget = STRDEF("../file")}, + {.function = HRNLIBSSH2_SFTP_SYMLINK_EX, .param = "[\"" TEST_PATH "/pg/link\",\"\",1]", + .resultInt = LIBSSH2_ERROR_NONE, .symlinkExTarget = STRDEF("../file")}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + // Create storage object for testing + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 4000, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB); + + storageTest->pub.interface.feature ^= 1 << storageFeatureInfoDetail; + + TEST_STORAGE_LIST( + storageTest, "pg", + "zzz/yyy/\n" + "zzz/\n" + "pipe*\n" + "path/file {s=8, t=1656434296}\n" + "path/\n" + "link> {d=../file}\n" + "file {s=8, t=1656433838}\n" + "./\n", + .levelForce = true, .includeDot = true, .sortOrder = sortOrderDesc); + + storageTest->pub.interface.feature ^= 1 << storageFeatureInfoDetail; + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("empty path - filter"); + + // Mimic "pg/empty" mode 0700 + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg\",0]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg\",0,0,1]"}, + // readdir returns dot + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("."), + .resultInt = 3, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir returns empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\".\",4095,null,0]", .fileName = STRDEF("empty"), + .resultInt = 5, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = 77777, .gid = 77777}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/empty\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IXGRP | + LIBSSH2_SFTP_S_IROTH | LIBSSH2_SFTP_S_IXOTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = 77777, .gid = 77777}, + // readdir returns path + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"empty\",4095,null,0]", .fileName = STRDEF("path"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = 77777, .gid = 77777}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/path\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IXGRP | + LIBSSH2_SFTP_S_IROTH | LIBSSH2_SFTP_S_IXOTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = 77777, .gid = 77777}, + // readdir returns file + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"path\",4095,null,0]", .fileName = STRDEF("file"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/file\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | + LIBSSH2_SFTP_S_IWGRP, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID | + LIBSSH2_SFTP_ATTR_SIZE, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .filesize = 8}, + // readdir returns link + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"file\",4095,null,0]", .fileName = STRDEF("link"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFLNK | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/link\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFLNK | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .symlinkExTarget = STRDEF("../file")}, + // readdir returns pipe + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"link\",4095,null,0]", .fileName = STRDEF("pipe"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFIFO | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/pipe\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFIFO | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir returns zzz + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"pipe\",4095,null,0]", .fileName = STRDEF("zzz"), + .resultInt = 3, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/zzz\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir return empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"zzz\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + // Recurse empty + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg/empty\",0,0,1]"}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + // Recurse path + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg/path\",0,0,1]"}, + // readdir returns file + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("file"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/path/file\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | + LIBSSH2_SFTP_S_IWGRP, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID | + LIBSSH2_SFTP_ATTR_SIZE, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .filesize = 8}, + // readdir returns empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"file\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + // Recurse zzz + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg/zzz\",0,0,1]"}, + // readdir returns yyy + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("yyy"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/zzz/yyy\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir returns empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"yyy\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + // Recurse yyy + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg/zzz/yyy\",0,0,1]"}, + // readdir returns empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + // Create storage object for testing + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 4000, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB); + + TEST_STORAGE_LIST(storageTest, "pg", + "empty/\n", + .level = storageInfoLevelType, .expression = "^empty"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("filter in subpath during recursion"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg\",0]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg\",0,0,1]"}, + // readdir returns dot + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("."), + .resultInt = 3, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir returns path + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\".\",4095,null,0]", .fileName = STRDEF("path"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = 77777, .gid = 77777}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/path\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IXGRP | + LIBSSH2_SFTP_S_IROTH | LIBSSH2_SFTP_S_IXOTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = 77777, .gid = 77777}, + // readdir returns file + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"path\",4095,null,0]", .fileName = STRDEF("file"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/file\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | + LIBSSH2_SFTP_S_IWGRP, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID | + LIBSSH2_SFTP_ATTR_SIZE, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .filesize = 8}, + // readdir returns link + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"file\",4095,null,0]", .fileName = STRDEF("link"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFLNK | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/link\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFLNK | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .symlinkExTarget = STRDEF("../file")}, + // readdir returns pipe + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"link\",4095,null,0]", .fileName = STRDEF("pipe"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFIFO | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/pipe\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFIFO | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir returns zzz + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"pipe\",4095,null,0]", .fileName = STRDEF("zzz"), + .resultInt = 3, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/zzz\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir return empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"zzz\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + // Recurse path + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg/path\",0,0,1]"}, + // readdir returns file + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("file"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/path/file\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | + LIBSSH2_SFTP_S_IWGRP, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID | + LIBSSH2_SFTP_ATTR_SIZE, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID, .filesize = 8}, + // readdir returns empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"file\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + // Recurse zzz + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg/zzz\",0,0,1]"}, + // readdir returns yyy + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("yyy"), + .resultInt = 4, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/pg/zzz/yyy\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // readdir returns empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"yyy\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656433838, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + // Recurse yyy + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/pg/zzz/yyy\",0,0,1]"}, + // readdir returns empty + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + // Create storage object for testing + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 4000, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB); + + TEST_STORAGE_LIST( + storageTest, "pg", + "path/file {s=8, t=1656434296}\n", + .level = storageInfoLevelBasic, .expression = "\\/file$"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + } + + // ***************************************************************************************************************************** + if (testBegin("storageList()")) + { + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("path missing"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/BOGUS\",0,0,1]", .resultNull = true}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/BOGUS\",0,0,1]", .resultNull = true}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + // Empty list + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/BOGUS\",0,0,1]", .resultNull = true}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + // Timeout on sftp_open_ex + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/BOGUS\",0,0,1]", .resultNull = true, + .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 23}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/BOGUS\",0,0,1]", .resultNull = true, + .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 4}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 4}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 4}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_OK}, + // Error on missing, regardless of errorOnMissing setting + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/noperm\",0,0,1]", .resultNull = true}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + // Ignore missing + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/noperm\",0,0,1]", .resultNull = true}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + // Create storage object for testing + Storage *storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 25, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB); + + TEST_ERROR_FMT( + storageListP(storageTest, STRDEF(BOGUS_STR), .errorOnMissing = true), PathMissingError, STORAGE_ERROR_LIST_INFO_MISSING, + TEST_PATH "/BOGUS"); + + TEST_RESULT_PTR(storageListP(storageTest, STRDEF(BOGUS_STR), .nullOnMissing = true), NULL, "null for missing dir"); + TEST_RESULT_UINT(strLstSize(storageListP(storageTest, STRDEF(BOGUS_STR))), 0, "empty list for missing dir"); + + // Timeout on sftp_open_ex + TEST_ERROR_FMT( + storageListP(storageTest, STRDEF(BOGUS_STR), .errorOnMissing = true), PathOpenError, + STORAGE_ERROR_LIST_INFO ": libssh2 error [-37]", TEST_PATH "/BOGUS"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("error on missing, regardless of errorOnMissing setting"); + + TEST_ERROR_FMT( + storageListP(storageTest, pathNoPerm, .errorOnMissing = true), PathOpenError, + STORAGE_ERROR_LIST_INFO ": libssh2 error [-31]: sftp error [3]", strZ(pathNoPerm)); + + // Should still error even when ignore missing + TEST_ERROR_FMT( + storageListP(storageTest, pathNoPerm), PathOpenError, + STORAGE_ERROR_LIST_INFO ": libssh2 error [-31]: sftp error [3]", strZ(pathNoPerm)); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("list - path with files"); + + ioBufferSizeSet(65536); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + // storagePutP(storageNewWriteP()) + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/.aaa.txt.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SFTP_WRITE, .param = "[14]", .resultInt = 14}, + {.function = HRNLIBSSH2_SFTP_FSYNC, .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_FSYNC, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_RENAME_EX, .resultInt = LIBSSH2_ERROR_EAGAIN, + .param = "[\"" TEST_PATH "/.aaa.txt.pgbackrest.tmp\",\"" TEST_PATH "/.aaa.txt\",7]"}, + {.function = HRNLIBSSH2_SFTP_RENAME_EX, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL, + .param = "[\"" TEST_PATH "/.aaa.txt.pgbackrest.tmp\",\"" TEST_PATH "/.aaa.txt\",7]"}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_FAILURE}, + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"" TEST_PATH "/.aaa.txt\"]", .resultUInt = LIBSSH2_FX_OK}, + {.function = HRNLIBSSH2_SFTP_RENAME_EX, .resultInt = LIBSSH2_ERROR_NONE, + .param = "[\"" TEST_PATH "/.aaa.txt.pgbackrest.tmp\",\"" TEST_PATH "/.aaa.txt\",7]"}, + // strListSort(storageListP()) + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "\",0,0,1]"}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF(".aaa.txt"), + .resultInt = 8, .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/.aaa.txt\",1]", .fileName = STRDEF(".aaa.txt"), + .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\".aaa.txt\",4095,null,0]", .fileName = STRDEF("noperm"), + .resultInt = 6, .uid = 0, .gid = 0}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/noperm\",1]", .fileName = STRDEF("noperm"), + .resultInt = LIBSSH2_ERROR_NONE, .uid = 0, .gid = 0}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"noperm\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .uid = 0, .gid = 0}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + // bbb.txt + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/bbb.txt.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SFTP_WRITE, .param = "[7]", .resultInt = 7}, + {.function = HRNLIBSSH2_SFTP_FSYNC, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_RENAME_EX, + .param = "[\"" TEST_PATH "/bbb.txt.pgbackrest.tmp\",\"" TEST_PATH "/bbb.txt\",7]", + .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "\",0,0,1]"}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("bbb.txt"), + .resultInt = 7, .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/bbb.txt\",1]", .fileName = STRDEF("bbb.txt"), + .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"bbb.txt\",4095,null,0]", .fileName = STRDEF("noperm"), + .resultInt = 6, .uid = 0, .gid = 0}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/noperm\",1]", .fileName = STRDEF("noperm"), + .resultInt = LIBSSH2_ERROR_NONE, .uid = 0, .gid = 0}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"noperm\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, .uid = 0, .gid = 0}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + // Path with only dot, readdir timeout EAGAIN + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "\",0,0,1]"}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .resultInt = LIBSSH2_ERROR_EAGAIN, .param = "[\"\",4095,null,0]", .sleep = 18}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .resultInt = LIBSSH2_ERROR_EAGAIN, .param = "[\"\",4095,null,0]", .sleep = 6}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + // Fail to close path after listing + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "\",0,0,1]"}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("."), + .resultInt = LIBSSH2_ERROR_NONE, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1555160000, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 18}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 5}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + // Create storage object for testing + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_RESULT_VOID( + storagePutP(storageNewWriteP(storageTest, STRDEF(".aaa.txt")), BUFSTRDEF("aaaaaaaaaaaaaa")), "write aaa.text"); + TEST_RESULT_STRLST_Z(strLstSort(storageListP(storageTest, NULL), sortOrderAsc), ".aaa.txt\n" "noperm\n", "dir list"); + + TEST_RESULT_VOID(storagePutP(storageNewWriteP(storageTest, STRDEF("bbb.txt")), BUFSTRDEF("bbb.txt")), "write bbb.text"); + TEST_RESULT_STRLST_Z(storageListP(storageTest, NULL, .expression = STRDEF("^bbb")), "bbb.txt\n", "dir list"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageSftpList() readdir EAGAIN timeout"); + + TEST_RESULT_VOID(storageListP(storageTest, NULL, .errorOnMissing = true), "storageSftpList readdir EAGAIN timeout"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageSftpList() fail to close path after listing"); + + TEST_ERROR_FMT( + storageListP(storageTest, NULL, .errorOnMissing = true), PathCloseError, + "unable to close path '" TEST_PATH "' after listing"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + } + + // ***************************************************************************************************************************** + if (testBegin("storagePath()")) + { + Storage *storageTest = NULL; + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("path - root path"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + TEST_ASSIGN( + storageTest, + storageSftpNewP(FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 1000, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB), + "new storage /"); + TEST_RESULT_STR_Z(storagePathP(storageTest, NULL), "/", "root dir"); + TEST_RESULT_STR_Z(storagePathP(storageTest, STRDEF("/")), "/", "same as root dir"); + TEST_RESULT_STR_Z(storagePathP(storageTest, STRDEF("subdir")), "/subdir", "simple subdir"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("path - expressions"); + + TEST_ERROR( + storagePathP(storageTest, STRDEF("")), AssertError, "expression '' not valid without callback function"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + TEST_ASSIGN( + storageTest, + storageSftpNewP( + STRDEF("/path/to"), STRDEF("localhost"), 22, TEST_USER_STR, 1000, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, + .pathExpressionFunction = storageTestPathExpression), + "new storage /path/to"); + TEST_RESULT_STR_Z(storagePathP(storageTest, NULL), "/path/to", "root dir"); + TEST_RESULT_STR_Z(storagePathP(storageTest, STRDEF("/path/to")), "/path/to", "absolute root dir"); + TEST_RESULT_STR_Z(storagePathP(storageTest, STRDEF("is/a/subdir")), "/path/to/is/a/subdir", "subdir"); + + TEST_ERROR( + storagePathP(storageTest, STRDEF("/bogus")), AssertError, "absolute path '/bogus' is not in base path '/path/to'"); + TEST_ERROR( + storagePathP(storageTest, STRDEF("/path/toot")), AssertError, + "absolute path '/path/toot' is not in base path '/path/to'"); + + // Path enforcement disabled for a single call + TEST_RESULT_STR_Z(storagePathP(storageTest, STRDEF("/bogus"), .noEnforce = true), "/bogus", "path enforce disabled"); + + TEST_ERROR(storagePathP(storageTest, STRDEF(" not found in path expression '" BOGUS_STR)), AssertError, + "'/' should separate expression and path 'BOGUS'"); + + TEST_RESULT_STR_Z(storagePathP(storageTest, STRDEF("")), "/path/to/test", "expression"); + TEST_ERROR(strZ(storagePathP(storageTest, STRDEF("/"))), AssertError, "path '/' should not end in '/'"); + + TEST_RESULT_STR_Z( + storagePathP(storageTest, STRDEF("/something")), "/path/to/test/something", "expression with path"); + + TEST_ERROR(storagePathP(storageTest, STRDEF("")), AssertError, "evaluated path '' cannot be null"); + + TEST_ERROR(storagePathP(storageTest, STRDEF("")), AssertError, "invalid expression ''"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + } + + // ***************************************************************************************************************************** + if (testBegin("storagePathCreate()")) + { + Storage *storageTest = NULL; + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + // Create /sub1 + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub1\",488]", .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub1\",488]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/sub1\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IXGRP, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID | + LIBSSH2_SFTP_ATTR_SIZE, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // reate again + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub1\",488]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_FAILURE}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/sub1\",0]", .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/sub1\",0]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IXGRP, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID | + LIBSSH2_SFTP_ATTR_SIZE, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // errorOnExists + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub1\",488]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_FAILURE}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/sub1\",0]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IXGRP, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID | + LIBSSH2_SFTP_ATTR_SIZE, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // sub2 custom mode + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub2\",511]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/sub2\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG | LIBSSH2_SFTP_S_IRWXO, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID | + LIBSSH2_SFTP_ATTR_SIZE, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // sub3/sub4 .noParentCreate fail + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub3/sub4\",488]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + // sub3/sub4 success + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub3/sub4\",488]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub3\",488]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub3/sub4\",488]", + .resultInt = LIBSSH2_ERROR_NONE}, + // subfail EAGAIN timeout + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/subfail\",488]", .resultInt = LIBSSH2_ERROR_EAGAIN, + .sleep = 50}, + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/subfail\",488]", .resultInt = LIBSSH2_ERROR_EAGAIN, + .sleep = 50}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + TEST_ASSIGN( + storageTest, + storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 25, KEYPRIV, hashTypeSha1, .write = true, .keyPub = KEYPUB), + "new storage /"); + TEST_RESULT_VOID(storagePathCreateP(storageTest, STRDEF("sub1")), "create sub1"); + TEST_RESULT_INT(storageInfoP(storageTest, STRDEF("sub1")).mode, 0750, "check sub1 dir mode"); + TEST_RESULT_VOID(storagePathCreateP(storageTest, STRDEF("sub1")), "create sub1 again"); + TEST_ERROR( + storagePathCreateP(storageTest, STRDEF("sub1"), .errorOnExists = true), PathCreateError, + "unable to create path '" TEST_PATH "/sub1': path already exists"); + + // NOTE: if operating against an actual sftp server, a neutral umask is required to get the proper permissions. + // Without the neutral umask, permissions were 0775. + TEST_RESULT_VOID(storagePathCreateP(storageTest, STRDEF("sub2"), .mode = 0777), "create sub2 with custom mode"); + TEST_RESULT_INT(storageInfoP(storageTest, STRDEF("sub2")).mode, 0777, "check sub2 dir mode"); + + TEST_ERROR( + storagePathCreateP(storageTest, STRDEF("sub3/sub4"), .noParentCreate = true), PathCreateError, + "sftp error unable to create path '" TEST_PATH "/sub3/sub4'"); + TEST_RESULT_VOID(storagePathCreateP(storageTest, STRDEF("sub3/sub4")), "create sub3/sub4"); + + // LIBSSH2_ERROR_EAGAIN timeout fail + TEST_ERROR( + storagePathCreateP(storageTest, STRDEF("subfail")), PathCreateError, + "ssh2 error [-37] unable to create path '" TEST_PATH "/subfail'"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("path create, timeout success on stat"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + // Timeout success + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/subfail\",488]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_FAILURE}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/subfail\",0]", .resultInt = LIBSSH2_ERROR_EAGAIN, + .sleep = 30}, + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/subfail\",0]", .resultInt = LIBSSH2_ERROR_EAGAIN}, + // Error other than no such file && no parent create LIBSSH2_FX_PERMISSION_DENIED} + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/subfail\",488]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + // No error on already exists + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/subfail\",488]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_FILE_ALREADY_EXISTS}, + // Error on already exists + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/subfail\",488]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_FILE_ALREADY_EXISTS}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + TEST_ASSIGN( + storageTest, + storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 25, KEYPRIV, hashTypeSha1, .write = true, .keyPub = KEYPUB), + "new storage /"); + TEST_RESULT_VOID(storagePathCreateP(storageTest, STRDEF("subfail")), "timeout success"); + TEST_ERROR( + storagePathCreateP(storageTest, STRDEF("subfail"), .noParentCreate = true), PathCreateError, + "sftp error unable to create path '" TEST_PATH "/subfail'"); + TEST_RESULT_VOID(storagePathCreateP(storageTest, STRDEF("subfail")), "do not throw error on already exists"); + TEST_ERROR( + storagePathCreateP(storageTest, STRDEF("subfail"), .errorOnExists = true), PathCreateError, + "sftp error unable to create path '" TEST_PATH "/subfail'"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + } + + // ***************************************************************************************************************************** + if (testBegin("storagePathRemove()")) + { + Storage *storageTest = NULL; + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + // Path remove missing errorOnMissing + {.function = HRNLIBSSH2_SFTP_RMDIR_EX, .param = "[\"" TEST_PATH "/remove1\"]", .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_RMDIR_EX, .param = "[\"" TEST_PATH "/remove1\"]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + // Recurse - ignore missing path + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/remove1\",0,0,1]", .resultNull = true}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_RMDIR_EX, .param = "[\"" TEST_PATH "/remove1\"]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + // Recurse parent/subpath permission denied + {.function = HRNLIBSSH2_SFTP_RMDIR_EX, .param = "[\"" TEST_PATH "/remove1/remove2\"]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + // Recurse subpath permission denied + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/remove1/remove2\",0,0,1]", .resultNull = true}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/remove1/remove2\",0,0,1]", .resultNull = true}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + // Path remove - file in subpath, permission denied + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/remove1\",0,0,1]"}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("remove2"), + .resultInt = 7, .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"remove2\",4095,null,0]", .fileName = STRDEF("remove.txt"), + .resultInt = 10, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = 0, .gid = 0}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"remove.txt\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = 0, .gid = 0}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"" TEST_PATH "/remove1/remove2\"]", + .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"" TEST_PATH "/remove1/remove2\"]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_FAILURE}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/remove1/remove2\",0,0,1]"}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("remove.txt"), + .resultInt = 10, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG | LIBSSH2_SFTP_S_IRWXO, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = 0, .gid = 0}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"remove.txt\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = 0, .gid = 0}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"" TEST_PATH "/remove1/remove2/remove.txt\"]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + // Path remove - path with subpath and file removed + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/remove1\",0,0,1]"}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("remove2"), .resultInt = 7, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG| LIBSSH2_SFTP_S_IRWXO, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"remove2\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = 0, .gid = 0}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"" TEST_PATH "/remove1/remove2\"]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_FAILURE}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/remove1/remove2\",0,0,1]"}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("remove.txt"), + .resultInt = 10, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG | LIBSSH2_SFTP_S_IRWXO, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = 0, .gid = 0}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"remove.txt\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = 0, .gid = 0}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"" TEST_PATH "/remove1/remove2/remove.txt\"]", + .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_RMDIR_EX, .param = "[\"" TEST_PATH "/remove1/remove2\"]", + .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_RMDIR_EX, .param = "[\"" TEST_PATH "/remove1/remove2\"]", + .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_RMDIR_EX, .param = "[\"" TEST_PATH "/remove1\"]", .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_RMDIR_EX, .param = "[\"" TEST_PATH "/remove1\"]", .resultInt = LIBSSH2_ERROR_NONE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + TEST_ASSIGN( + storageTest, + storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 250, KEYPRIV, hashTypeSha1, .write = true, .keyPub = KEYPUB), + "new storage /"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("path remove - missing"); + + const String *pathRemove1 = STRDEF(TEST_PATH "/remove1"); + + TEST_ERROR_FMT( + storagePathRemoveP(storageTest, pathRemove1, .errorOnMissing = true), PathRemoveError, + STORAGE_ERROR_PATH_REMOVE_MISSING, strZ(pathRemove1)); + TEST_RESULT_VOID(storagePathRemoveP(storageTest, pathRemove1, .recurse = true), "ignore missing path"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("path remove - parent/subpath permission denied"); + + String *pathRemove2 = strNewFmt("%s/remove2", strZ(pathRemove1)); + + // Mimic creation of pathRemove2 mode 700 + TEST_ERROR_FMT(storagePathRemoveP(storageTest, pathRemove2), PathRemoveError, STORAGE_ERROR_PATH_REMOVE, strZ(pathRemove2)); + TEST_ERROR_FMT( + storagePathRemoveP(storageTest, pathRemove2, .recurse = true), PathOpenError, + STORAGE_ERROR_LIST_INFO ": libssh2 error [-31]: sftp error [3]", strZ(pathRemove2)); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("path remove - subpath permission denied"); + + // Mimic chmod 777 pathRemove1 + TEST_ERROR_FMT( + storagePathRemoveP(storageTest, pathRemove2, .recurse = true), PathOpenError, + STORAGE_ERROR_LIST_INFO ": libssh2 error [-31]: sftp error [3]", strZ(pathRemove2)); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("path remove - file in subpath, permission denied"); + + String *fileRemove = strNewFmt("%s/remove.txt", strZ(pathRemove2)); + + // Mimic "sudo chmod 755 %s && sudo touch %s && sudo chmod 777 %s", strZ(pathRemove2), strZ(fileRemove), strZ(fileRemove)); + TEST_ERROR_FMT( + storagePathRemoveP(storageTest, pathRemove1, .recurse = true), PathRemoveError, STORAGE_ERROR_PATH_REMOVE_FILE, + strZ(fileRemove)); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("path remove - path with subpath and file removed"); + + // Mimic chmod 777 pathRemove2 + TEST_RESULT_VOID(storagePathRemoveP(storageTest, pathRemove1, .recurse = true), "remove path"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("path remove - unlink LIBSSH2_ERROR_EAGAIN timeout"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/remove1\",0,0,1]"}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("remove2"), .resultInt = 7, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG| LIBSSH2_SFTP_S_IRWXO, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"remove2\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = 0, .gid = 0}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"" TEST_PATH "/remove1/remove2\"]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_FAILURE}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/remove1/remove2\",0,0,1]"}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF("remove.txt"), + .resultInt = 10, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRWXG | LIBSSH2_SFTP_S_IRWXO, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = 0, .gid = 0}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"remove.txt\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP | + LIBSSH2_SFTP_S_IROTH, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = 0, .gid = 0}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"" TEST_PATH "/remove1/remove2/remove.txt\"]", + .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 30}, + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"" TEST_PATH "/remove1/remove2/remove.txt\"]", + .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 30}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + TEST_ASSIGN( + storageTest, + storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 25, KEYPRIV, hashTypeSha1, .write = true, .keyPub = KEYPUB), + "new storage /"); + TEST_ERROR_FMT( + storagePathRemoveP(storageTest, pathRemove1, .recurse = true), PathRemoveError, STORAGE_ERROR_PATH_REMOVE_FILE, + strZ(fileRemove)); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("path remove - rmdir LIBSSH2_ERROR_EAGAIN timeout"); + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/remove1\",0,0,1]"}, + {.function = HRNLIBSSH2_SFTP_READDIR_EX, .param = "[\"\",4095,null,0]", .fileName = STRDEF(""), + .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_RMDIR_EX, .param = "[\"" TEST_PATH "/remove1\"]", .resultInt = LIBSSH2_ERROR_EAGAIN, + .sleep = 30}, + {.function = HRNLIBSSH2_SFTP_RMDIR_EX, .param = "[\"" TEST_PATH "/remove1\"]", .resultInt = LIBSSH2_ERROR_EAGAIN, + .sleep = 30}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + TEST_ASSIGN( + storageTest, + storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 25, KEYPRIV, hashTypeSha1, .write = true, .keyPub = KEYPUB), + "new storage /"); + TEST_ERROR_FMT( + storagePathRemoveP(storageTest, pathRemove1, .recurse = true), PathRemoveError, STORAGE_ERROR_PATH_REMOVE, + strZ(pathRemove1)); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + } + + // ***************************************************************************************************************************** + if (testBegin("storageNewRead()")) + { + Storage *storageTest = NULL; + StorageRead *file = NULL; + const String *fileName = STRDEF(TEST_PATH "/readtest.txt"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + // Missing sftp error no such file + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/readtest.txt\",1,0,0]", .resultNull = true, + .sleep = 23}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/readtest.txt\",1,0,0]", .resultNull = true, + .sleep = 5}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + // Timeout EAGAIN + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/readtest.txt\",1,0,0]", .resultNull = true, + .sleep = 23}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/readtest.txt\",1,0,0]", .resultNull = true, + .sleep = 5}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_OK}, + // Error not sftp, not EAGAIN + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/readtest.txt\",1,0,0]", .resultNull = true, + .sleep = 23}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/readtest.txt\",1,0,0]", .resultNull = true, + .sleep = 5}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_METHOD_NOT_SUPPORTED}, + // Read success + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/readtest.txt\",1,0,0]"}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + TEST_ASSIGN( + storageTest, + storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 25, KEYPRIV, hashTypeSha1, .write = true, .keyPub = KEYPUB), + "new storage /"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("read missing"); + + TEST_ASSIGN(file, storageNewReadP(storageTest, fileName), "new read file (defaults)"); + + // Missing no such file + TEST_ERROR_FMT(ioReadOpen(storageReadIo(file)), FileMissingError, STORAGE_ERROR_READ_MISSING, strZ(fileName)); + + // Missing EAGAIN timeout + TEST_ERROR_FMT(ioReadOpen(storageReadIo(file)), FileOpenError, STORAGE_ERROR_READ_OPEN, strZ(fileName)); + + // Missing not sftp, not EAGAIN + TEST_RESULT_BOOL(ioReadOpen(storageReadIo(file)), false, "not sftp, not EAGAIN"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("read success"); + + // Mimic creation of TEST_PATH "/readtest.txt" + TEST_RESULT_BOOL(ioReadOpen(storageReadIo(file)), true, "open file"); + TEST_RESULT_INT(ioReadFd(storageReadIo(file)), -1, "check read fd"); + TEST_RESULT_VOID(ioReadClose(storageReadIo(file)), "close file"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + } + + // ***************************************************************************************************************************** + if (testBegin("storageReadClose()")) + { + // ------------------------------------------------------------------------------------------------------------------------ + TEST_TITLE("close success via storageReadSftpClose()"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + // storageReadSftpClose + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/readtest.txt\",1,0,0]"}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + // close(ioSessionFd()...) + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + Storage *storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 25, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + StorageRead *file = NULL; + const String *fileName = STRDEF(TEST_PATH "/readtest.txt"); + + // Mimic creation of fileName + Buffer *outBuffer = bufNew(2); + + TEST_ASSIGN(file, storageNewReadP(storageTest, fileName), "new read file (defaults)"); + TEST_RESULT_BOOL(ioReadOpen(storageReadIo(file)), true, "open file"); + TEST_RESULT_VOID(storageReadSftpClose((StorageReadSftp *)file->driver), "close file"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------ + TEST_TITLE("close success via close()"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + // close(ioSessionFd()...) + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/readtest.txt\",1,0,0]"}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 25, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(file, storageNewReadP(storageTest, fileName), "new read file (defaults)"); + TEST_RESULT_BOOL(ioReadOpen(storageReadIo(file)), true, "open file"); + close(ioSessionFd(((StorageReadSftp *)file->driver)->ioSession)); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------ + TEST_TITLE("close, null sftpHandle"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + // storageReadSftpClose null sftpHandle + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/readtest.txt\",1,0,0]"}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 25, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(file, storageNewReadP(storageTest, fileName), "new read file (defaults)"); + TEST_RESULT_BOOL(ioReadOpen(storageReadIo(file)), true, "open file"); + ((StorageReadSftp *)file->driver)->sftpHandle = NULL; + TEST_RESULT_VOID(storageReadSftpClose((StorageReadSftp *)file->driver), "close file null sftpHandle"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------ + TEST_TITLE("close fail via storageReadSftpClose() EAGAIN"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + // storageReadSftpClose + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/readtest.txt\",1,0,0]"}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 23}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 5}, + // close(ioSessionFd()...) + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 25, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(file, storageNewReadP(storageTest, fileName), "new read file (defaults)"); + TEST_RESULT_BOOL(ioReadOpen(storageReadIo(file)), true, "open file"); + TEST_ERROR( + storageReadSftpClose((StorageReadSftp *)file->driver), FileCloseError, + "unable to close file '" TEST_PATH "/readtest.txt' after read: libssh2 errno [-37]"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------ + TEST_TITLE("close fail via storageReadSftpClose() sftp error"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + // storageReadSftpClose + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/readtest.txt\",1,0,0]"}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 23}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL, .sleep = 5}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_FAILURE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 25, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(file, storageNewReadP(storageTest, fileName), "new read file (defaults)"); + TEST_RESULT_BOOL(ioReadOpen(storageReadIo(file)), true, "open file"); + TEST_ERROR( + storageReadSftpClose((StorageReadSftp *)file->driver), FileCloseError, + "unable to close file '" TEST_PATH "/readtest.txt' after read: libssh2 errno [-31]: sftp errno [4]"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------ + TEST_TITLE("storageReadSftp()"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + // storageReadSftpClose + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/readtest.txt\",1,0,0]"}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_FAILURE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(file, storageNewReadP(storageTest, fileName), "new read file (defaults)"); + TEST_RESULT_BOOL(ioReadOpen(storageReadIo(file)), true, "open file"); + TEST_ERROR( + storageReadSftp(((StorageReadSftp *)file->driver), outBuffer, false), FileReadError, + "unable to read '" TEST_PATH "/readtest.txt': sftp errno [4]"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + } + + // ***************************************************************************************************************************** + if (testBegin("storageNewWrite()")) + { + const String *fileName = STRDEF(TEST_PATH "/sub1/testfile"); + StorageWrite *file = NULL; + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("permission denied"); + + // Mimic creation of /noperm/noperm + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + // Permission denied + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/noperm/noperm\",26,416,0]", .resultNull = true, + .sleep = 18}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/noperm/noperm\",26,416,0]", .resultNull = true, + .sleep = 18, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + Storage *storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(file, storageNewWriteP(storageTest, fileNoPerm, .noAtomic = true, .noCreatePath = false), "new write file"); + TEST_ERROR_FMT( + ioWriteOpen(storageWriteIo(file)), FileOpenError, + STORAGE_ERROR_WRITE_OPEN ": libssh2 error [-31]: sftp error [3]", strZ(fileNoPerm)); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("timeout"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + // Timeout + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/noperm/noperm\",26,416,0]", .resultNull = true, + .sleep = 18}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/noperm/noperm\",26,416,0]", .resultNull = true, + .sleep = 18}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_EAGAIN}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(file, storageNewWriteP(storageTest, fileNoPerm, .noAtomic = true, .noCreatePath = false), "new write file"); + TEST_ERROR_FMT(ioWriteOpen(storageWriteIo(file)), FileOpenError, STORAGE_ERROR_WRITE_OPEN, strZ(fileNoPerm)); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("missing"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + // Missing + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/missing\",26,416,0]", .resultNull = true, + .sleep = 18}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "\",488]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/missing\",26,416,0]", .resultNull = true, .sleep = 18}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN( + file, storageNewWriteP(storageTest, STRDEF("missing"), .noAtomic = true, .noCreatePath = false), "new write file"); + TEST_ERROR_FMT(ioWriteOpen(storageWriteIo(file)), FileMissingError, STORAGE_ERROR_WRITE_MISSING, TEST_PATH "/missing"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("write file - defaults"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .resultNull = true, .sleep = 18, + .param = "[\"" TEST_PATH "/sub1/testfile.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub1\",488]"}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/sub1/testfile.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SFTP_FSYNC, .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_FSYNC, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_RENAME_EX, + .param = "[\"" TEST_PATH "/sub1/testfile.pgbackrest.tmp\",\"" TEST_PATH "/sub1/testfile\",7]", + .resultInt = LIBSSH2_ERROR_NONE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(file, storageNewWriteP(storageTest, fileName), "new write file (defaults)"); + TEST_RESULT_VOID(ioWriteOpen(storageWriteIo(file)), "open file"); + TEST_RESULT_INT(ioWriteFd(storageWriteIo(file)), -1, "check write fd"); + TEST_RESULT_VOID(ioWriteClose(storageWriteIo(file)), "close file"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("timeout on fsync"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .resultNull = true, .sleep = 18, + .param = "[\"" TEST_PATH "/sub1/testfile.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub1\",488]"}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/sub1/testfile.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SFTP_FSYNC, .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 20}, + {.function = HRNLIBSSH2_SFTP_FSYNC, .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 7}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + const String *fileNameTmp = STRDEF(TEST_PATH "/sub1/testfile.pgbackrest.tmp"); + + TEST_ASSIGN(file, storageNewWriteP(storageTest, fileName), "new write file (defaults)"); + TEST_RESULT_VOID(ioWriteOpen(storageWriteIo(file)), "open file"); + TEST_RESULT_INT(ioWriteFd(storageWriteIo(file)), -1, "check write fd"); + TEST_ERROR_FMT(ioWriteClose(storageWriteIo(file)), FileSyncError, STORAGE_ERROR_WRITE_SYNC, strZ(fileNameTmp)); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("timeout on rename"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .resultNull = true, .sleep = 18, + .param = "[\"" TEST_PATH "/sub1/testfile.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub1\",488]"}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/sub1/testfile.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SFTP_FSYNC, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_RENAME_EX, + .param = "[\"" TEST_PATH "/sub1/testfile.pgbackrest.tmp\",\"" TEST_PATH "/sub1/testfile\",7]", + .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 18}, + {.function = HRNLIBSSH2_SFTP_RENAME_EX, + .param = "[\"" TEST_PATH "/sub1/testfile.pgbackrest.tmp\",\"" TEST_PATH "/sub1/testfile\",7]", + .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 5}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_OK}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(file, storageNewWriteP(storageTest, fileName), "new write file (defaults)"); + TEST_RESULT_VOID(ioWriteOpen(storageWriteIo(file)), "open file"); + TEST_RESULT_INT(ioWriteFd(storageWriteIo(file)), -1, "check write fd"); + TEST_ERROR_FMT( + ioWriteClose(storageWriteIo(file)), + FileCloseError, "unable to move '%s' to '%s': libssh2 error [-37]", strZ(fileNameTmp), strZ(fileName)); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("sftp error on rename, other than LIBSSH2_FX_FAILURE"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .resultNull = true, .sleep = 18, + .param = "[\"" TEST_PATH "/sub1/testfile.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub1\",488]"}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/sub1/testfile.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SFTP_FSYNC, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_RENAME_EX, + .param = "[\"" TEST_PATH "/sub1/testfile.pgbackrest.tmp\",\"" TEST_PATH "/sub1/testfile\",7]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_CONNECTION_LOST}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_CONNECTION_LOST}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(file, storageNewWriteP(storageTest, fileName), "new write file (defaults)"); + TEST_RESULT_VOID(ioWriteOpen(storageWriteIo(file)), "open file"); + TEST_RESULT_INT(ioWriteFd(storageWriteIo(file)), -1, "check write fd"); + TEST_ERROR_FMT( + ioWriteClose(storageWriteIo(file)), FileCloseError, + "unable to move '%s' to '%s': libssh2 error [-31]: sftp error [7]", strZ(fileNameTmp), strZ(fileName)); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageWriteSftpClose() timeout EAGAIN on libssh2_sftp_close"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .resultNull = true, .sleep = 18, + .param = "[\"" TEST_PATH "/sub1/testfile.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub1\",488]"}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/sub1/testfile.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SFTP_FSYNC, .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_FSYNC, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 18}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 5}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(file, storageNewWriteP(storageTest, fileName), "new write file (defaults)"); + TEST_RESULT_VOID(ioWriteOpen(storageWriteIo(file)), "open file"); + TEST_RESULT_INT(ioWriteFd(storageWriteIo(file)), -1, "check write fd"); + TEST_ERROR_FMT( + ioWriteClose(storageWriteIo(file)), FileCloseError, + STORAGE_ERROR_WRITE_CLOSE ": libssh2 error [0] ", strZ(fileNameTmp)); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("write file - noAtomic = true"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .resultNull = true, + .param = "[\"" TEST_PATH "/sub1/testfile\",26,416,0]"}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub1\",488]"}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/sub1/testfile\",26,416,0]"}, + {.function = HRNLIBSSH2_SFTP_FSYNC, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(file, storageNewWriteP(storageTest, fileName, .noAtomic = true), "new write file (defaults)"); + TEST_RESULT_VOID(ioWriteOpen(storageWriteIo(file)), "open file"); + TEST_RESULT_INT(ioWriteFd(storageWriteIo(file)), -1, "check write fd"); + TEST_RESULT_VOID(ioWriteClose(storageWriteIo(file)), "close file"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("write file - syncPath not NULL, .noSyncPath = true"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .resultNull = true, .param = "[\"" TEST_PATH "/sub1/testfile\",26,416,0]"}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub1\",488]"}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/sub1/testfile\",26,416,0]"}, + {.function = HRNLIBSSH2_SFTP_FSYNC, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + // Make interface.pathSync != NULL + ((StorageSftp *)storageDriver(storageTest))->interface.pathSync = malloc(1); + + TEST_ASSIGN( + file, storageNewWriteP(storageTest, fileName, .noAtomic = true, .noSyncPath = true), "new write file (defaults)"); + TEST_RESULT_VOID(ioWriteOpen(storageWriteIo(file)), "open file"); + TEST_RESULT_INT(ioWriteFd(storageWriteIo(file)), -1, "check write fd"); + TEST_RESULT_VOID(ioWriteClose(storageWriteIo(file)), "close file"); + + free(((StorageSftp *)storageDriver(storageTest))->interface.pathSync); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("write file - syncPath not NULL, .noSyncPath = false"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .resultNull = true, .param = "[\"" TEST_PATH "/sub1/testfile\",26,416,0]"}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub1\",488]"}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/sub1/testfile\",26,416,0]"}, + {.function = HRNLIBSSH2_SFTP_FSYNC, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + // Make interface.pathSync != NULL + ((StorageSftp *)storageDriver(storageTest))->interface.pathSync = malloc(1); + + TEST_ASSIGN(file, storageNewWriteP(storageTest, fileName, .noAtomic = true, .noSyncPath = false), "new write file"); + TEST_RESULT_VOID(ioWriteOpen(storageWriteIo(file)), "open file"); + TEST_RESULT_INT(ioWriteFd(storageWriteIo(file)), -1, "check write fd"); + TEST_RESULT_VOID(ioWriteClose(storageWriteIo(file)), "close file"); + + free(((StorageSftp *)storageDriver(storageTest))->interface.pathSync); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("write file - syncFile false"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .resultNull = true, .param = "[\"" TEST_PATH "/sub1/testfile\",26,416,0]"}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub1\",488]"}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/sub1/testfile\",26,416,0]"}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + ((StorageSftp *)storageDriver(storageTest))->interface.pathSync = malloc(1); + + TEST_ASSIGN(file, storageNewWriteP(storageTest, fileName, .noAtomic = true, .noSyncPath = false), "new write file"); + ((StorageWriteSftp *)file->driver)->interface.syncFile = false; + TEST_RESULT_VOID(ioWriteOpen(storageWriteIo(file)), "open file"); + TEST_RESULT_INT(ioWriteFd(storageWriteIo(file)), -1, "check write fd"); + TEST_RESULT_VOID(ioWriteClose(storageWriteIo(file)), "close file"); + + free(((StorageSftp *)storageDriver(storageTest))->interface.pathSync); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageWriteSftpClose() - null sftpHandle"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .resultNull = true, .param = "[\"" TEST_PATH "/sub1/testfile\",26,416,0]"}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub1\",488]"}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/sub1/testfile\",26,416,0]"}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(file, storageNewWriteP(storageTest, fileName, .noAtomic = true, .noSyncPath = false), "new write file"); + TEST_RESULT_VOID(ioWriteOpen(storageWriteIo(file)), "open file"); + TEST_RESULT_INT(ioWriteFd(storageWriteIo(file)), -1, "check write fd"); + + // Make sftpHandle NULL + ((StorageWriteSftp *)file->driver)->sftpHandle = NULL; + + TEST_RESULT_VOID(ioWriteClose(storageWriteIo(file)), "close file"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + } + + // ***************************************************************************************************************************** + if (testBegin("storagePut() and storageGet()")) + { + ioBufferSizeSet(65536); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "\",1,0,0]"}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[65536]", .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 18}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[65536]", .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL, .sleep = 5}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_FAILURE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + Storage *storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("get error - attempt to get directory"); + + TEST_ERROR( + storageGetP(storageNewReadP(storageTest, TEST_PATH_STR, .ignoreMissing = false)), FileReadError, + "unable to read '" TEST_PATH "': sftp errno [4]"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("put - empty file"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.empty.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SFTP_FSYNC, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_RENAME_EX, + .param = "[\"" TEST_PATH "/test.empty.pgbackrest.tmp\",\"" TEST_PATH "/test.empty\",7]", + .resultInt = LIBSSH2_ERROR_NONE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + const String *emptyFile = STRDEF(TEST_PATH "/test.empty"); + TEST_RESULT_VOID(storagePutP(storageNewWriteP(storageTest, emptyFile), NULL), "put empty file"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("put - empty file, already exists"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.empty.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SFTP_FSYNC, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_RENAME_EX, + .param = "[\"" TEST_PATH "/test.empty.pgbackrest.tmp\",\"" TEST_PATH "/test.empty\",7]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL, .sleep = 18}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_FAILURE}, + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"" TEST_PATH "/test.empty\"]", .resultInt = LIBSSH2_ERROR_EAGAIN, + .sleep = 18}, + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"" TEST_PATH "/test.empty\"]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL, .sleep = 5}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_OK}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ERROR( + storagePutP(storageNewWriteP(storageTest, emptyFile), NULL), FileRemoveError, + "unable to remove existing '" TEST_PATH "/test.empty': libssh2 error [-31]: sftp error [0]"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("put - empty file, remove already existing file, storageWriteSftpRename timeout EAGAIN"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.empty.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SFTP_FSYNC, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_RENAME_EX, + .param = "[\"" TEST_PATH "/test.empty.pgbackrest.tmp\",\"" TEST_PATH "/test.empty\",7]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL, .sleep = 18}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_FAILURE}, + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"" TEST_PATH "/test.empty\"]", .resultInt = LIBSSH2_ERROR_EAGAIN, + .sleep = 18}, + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"" TEST_PATH "/test.empty\"]", .resultInt = LIBSSH2_ERROR_NONE, + .sleep = 5}, + {.function = HRNLIBSSH2_SFTP_RENAME_EX, + .param = "[\"" TEST_PATH "/test.empty.pgbackrest.tmp\",\"" TEST_PATH "/test.empty\",7]", + .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 18}, + {.function = HRNLIBSSH2_SFTP_RENAME_EX, + .param = "[\"" TEST_PATH "/test.empty.pgbackrest.tmp\",\"" TEST_PATH "/test.empty\",7]", + .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 5}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_OK}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ERROR( + storagePutP(storageNewWriteP(storageTest, emptyFile), NULL), FileRemoveError, + "unable to move '" TEST_PATH "/test.empty.pgbackrest.tmp' to '" TEST_PATH "/test.empty': libssh2 error [-37]"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("put - empty file, EAGAIN fail on remove already existing file"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.empty.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SFTP_FSYNC, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_RENAME_EX, + .param = "[\"" TEST_PATH "/test.empty.pgbackrest.tmp\",\"" TEST_PATH "/test.empty\",7]", + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL, .sleep = 18}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_FAILURE}, + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"" TEST_PATH "/test.empty\"]", .resultInt = LIBSSH2_ERROR_EAGAIN, + .sleep = 18}, + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"" TEST_PATH "/test.empty\"]", .resultInt = LIBSSH2_ERROR_EAGAIN, + .sleep = 5}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_OK}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ERROR( + storagePutP(storageNewWriteP(storageTest, emptyFile), NULL), FileRemoveError, + "unable to remove existing '" TEST_PATH "/test.empty': libssh2 error [-37]"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("put - file with contents - timeout EAGAIN libssh2_sftp_write"); + + ioBufferSizeSet(2); + + const Buffer *failBuffer = BUFSTRDEF("FAIL\n"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.txt.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SFTP_WRITE, .param = "[2]", .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 18}, + {.function = HRNLIBSSH2_SFTP_WRITE, .param = "[2]", .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 5}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ERROR( + storagePutP(storageNewWriteP(storageTest, STRDEF(TEST_PATH "/test.txt")), failBuffer), FileWriteError, + "unable to write '" TEST_PATH "/test.txt.pgbackrest.tmp'"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("put - file with contents"); + + const Buffer *buffer = BUFSTRDEF("TESTFILE\n"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.txt.pgbackrest.tmp\",26,416,0]"}, + // Not passing buffer param, see shim function, initial passed buffer contains random data + {.function = HRNLIBSSH2_SFTP_WRITE, .param = "[2]", .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 18}, + {.function = HRNLIBSSH2_SFTP_WRITE, .param = "[2]", .resultInt = 2, .sleep = 5}, + {.function = HRNLIBSSH2_SFTP_WRITE, .param = "[2]", .resultInt = 2}, + {.function = HRNLIBSSH2_SFTP_WRITE, .param = "[2]", .resultInt = 2}, + {.function = HRNLIBSSH2_SFTP_WRITE, .param = "[2]", .resultInt = 1}, + {.function = HRNLIBSSH2_SFTP_WRITE, .param = "[1]", .resultInt = 1}, + {.function = HRNLIBSSH2_SFTP_WRITE, .param = "[1]", .resultInt = 1}, + {.function = HRNLIBSSH2_SFTP_FSYNC, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_RENAME_EX, + .param = "[\"" TEST_PATH "/test.txt.pgbackrest.tmp\",\"" TEST_PATH "/test.txt\",7]", + .resultInt = LIBSSH2_ERROR_NONE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_RESULT_VOID(storagePutP(storageNewWriteP(storageTest, STRDEF(TEST_PATH "/test.txt")), buffer), "put test file"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("get - ignore missing"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/BOGUS\",1,0,0]", .resultNull = true, .sleep = 18}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/BOGUS\",1,0,0]", .resultNull = true, .sleep = 5}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_RESULT_PTR( + storageGetP(storageNewReadP(storageTest, STRDEF(TEST_PATH "/" BOGUS_STR), .ignoreMissing = true)), NULL, + "get missing file"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("get - empty file"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.empty\",1,0,0]"}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(buffer, storageGetP(storageNewReadP(storageTest, STRDEF(TEST_PATH "/test.empty"))), "get empty"); + TEST_RESULT_UINT(bufSize(buffer), 0, "size is 0"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("get - file with contents"); + + const Buffer *outBuffer = bufNew(2); + + ioBufferSizeSet(65536); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.txt\",1,0,0]"}, + // Not passing buffer param, see shim function, initial passed buffer contains random data + {.function = HRNLIBSSH2_SFTP_READ, .param = "[65536]", .resultInt = 9, .readBuffer = STRDEF("TESTFILE\n")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[65527]", .resultInt = 0}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(outBuffer, storageGetP(storageNewReadP(storageTest, STRDEF(TEST_PATH "/test.txt"))), "get text"); + TEST_RESULT_UINT(bufSize(outBuffer), 9, "check size"); + TEST_RESULT_BOOL(memcmp(bufPtrConst(outBuffer), "TESTFILE\n", bufSize(outBuffer)) == 0, true, "check content"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("get - exact size smaller"); + + ioBufferSizeSet(2); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.txt\",1,0,0]"}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = 2, .readBuffer = STRDEF("TE")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = 2, .readBuffer = STRDEF("ST")}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(buffer, storageGetP(storageNewReadP(storageTest, STRDEF(TEST_PATH "/test.txt")), .exactSize = 4), "get exact"); + TEST_RESULT_UINT(bufSize(buffer), 4, "check size"); + TEST_RESULT_BOOL(memcmp(bufPtrConst(buffer), "TEST", bufSize(buffer)) == 0, true, "check content"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("get - exact size larger"); + + ioBufferSizeSet(4); + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.txt\",1,0,0]"}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[4]", .resultInt = 4, .readBuffer = STRDEF("TEST")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[4]", .resultInt = 4, .readBuffer = STRDEF("FILE")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[4]", .resultInt = 1, .readBuffer = STRDEF("\n")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[3]", .resultInt = 0}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ERROR( + storageGetP(storageNewReadP(storageTest, STRDEF(TEST_PATH "/test.txt")), .exactSize = 64), FileReadError, + "unable to read 64 byte(s) from '" TEST_PATH "/test.txt'"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("get - smaller buffer size"); + + ioBufferSizeSet(2); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.txt\",1,0,0]"}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = 2, .readBuffer = STRDEF("TE")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = 2, .readBuffer = STRDEF("ST")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = 2, .readBuffer = STRDEF("FI")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = 2, .readBuffer = STRDEF("LE")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = 1, .readBuffer = STRDEF("\n")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[1]", .resultInt = 0}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(buffer, storageGetP(storageNewReadP(storageTest, STRDEF(TEST_PATH "/test.txt"))), "get text"); + TEST_RESULT_UINT(bufSize(buffer), 9, "check size"); + TEST_RESULT_BOOL(memcmp(bufPtrConst(buffer), "TESTFILE\n", bufSize(buffer)) == 0, true, "check content"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("error on invalid read offset bytes"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.txt\",1,0,0]"}, + {.function = HRNLIBSSH2_SFTP_SEEK64, .param = "[18446744073709551615]"}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_BAD_MESSAGE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ERROR( + storageGetP(storageNewReadP(storageTest, STRDEF(TEST_PATH "/test.txt"), .offset = UINT64_MAX)), FileOpenError, + "unable to seek to 18446744073709551615 in file '" TEST_PATH "/test.txt'"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("error on invalid read"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.txt\",1,0,0]"}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_BAD_MESSAGE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ERROR( + storageGetP(storageNewReadP(storageTest, STRDEF(TEST_PATH "/test.txt"))), FileReadError, + "unable to read '" TEST_PATH "/test.txt': sftp errno [5]"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("read limited bytes"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.txt\",1,0,0]"}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = 2, .readBuffer = STRDEF("TE")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = 2, .readBuffer = STRDEF("ST")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = 2, .readBuffer = STRDEF("FI")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[1]", .resultInt = 1, .readBuffer = STRDEF("LE")}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(buffer, storageGetP(storageNewReadP(storageTest, STRDEF(TEST_PATH "/test.txt"), .limit = VARUINT64(7))), "get"); + TEST_RESULT_UINT(bufSize(buffer), 7, "check size"); + TEST_RESULT_BOOL(memcmp(bufPtrConst(buffer), "TESTFIL", bufSize(buffer)) == 0, true, "check content"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("read offset bytes"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.txt\",1,0,0]"}, + {.function = HRNLIBSSH2_SFTP_SEEK64, .param = "[4]"}, + // Simulate seeking offset 4 + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = 2, .readBuffer = STRDEF("FI")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = 2, .readBuffer = STRDEF("LE")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = 1, .readBuffer = STRDEF("\n")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[1]", .resultInt = 0}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(buffer, storageGetP(storageNewReadP(storageTest, STRDEF(TEST_PATH "/test.txt"), .offset = 4)), "get"); + TEST_RESULT_UINT(bufSize(buffer), 5, "check size"); + TEST_RESULT_BOOL(memcmp(bufPtrConst(buffer), "FILE\n", bufSize(buffer)) == 0, true, "check content"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("get - read timeout EAGAIN"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.txt\",1,0,0]"}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 18}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 5}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ERROR( + storageGetP(storageNewReadP(storageTest, STRDEF(TEST_PATH "/test.txt"))), FileReadError, + "unable to read '" TEST_PATH "/test.txt'"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + } + + // ***************************************************************************************************************************** + if (testBegin("storageRemove()")) + { + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("remove - file missing"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"/missing\"]", .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"/missing\"]", .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + Storage *storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_RESULT_VOID(storageRemoveP(storageTest, STRDEF("missing")), "remove missing file"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("remove - file missing, .errorOnMissing = true, sftp error LIBSSH2_FX_NO_SUCH_FILE"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"/missing\"]", .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"/missing\"]", .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ERROR(storageRemoveP(storageTest, STRDEF("missing"), .errorOnMissing = true), FileRemoveError, + "unable to remove '/missing': libssh2 error [-31]: sftp error [2]"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("remove - file missing, sftp error other than LIBSSH2_FX_NO_SUCH_FILE"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"/missing\"]", .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"/missing\"]", .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_CONNECTION_LOST}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_CONNECTION_LOST}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ERROR(storageRemoveP(storageTest, STRDEF("missing")), FileRemoveError, + "unable to remove '/missing': libssh2 error [-31]: sftp error [7]"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("remove - file missing, ssh error, .errorOnMissing = true"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"/missing\"]", .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"/missing\"]", .resultInt = LIBSSH2_ERROR_BAD_SOCKET}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ERROR(storageRemoveP(storageTest, STRDEF("missing"), .errorOnMissing = true), FileRemoveError, + "unable to remove '/missing'"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("remove - LIBSSH2_ERROR_EAGAIN"); + + const String *fileRemove1 = STRDEF(TEST_PATH "/remove.txt"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"" TEST_PATH "/remove.txt\"]", + .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 15}, + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"" TEST_PATH "/remove.txt\"]", + .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 3}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + TEST_ASSIGN( + storageTest, + storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 10, KEYPRIV, hashTypeSha1, .write = true, .keyPub = KEYPUB), + "new storage /"); + TEST_RESULT_VOID(storageRemoveP(storageTest, fileRemove1), "remove file"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageWriteSftpOpen libssh2_sftp_open_ex timeout EAGAIN"); + + StorageWrite *file = NULL; + const String *fileName = STRDEF(TEST_PATH "/sub1/testfile"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .resultNull = true, .sleep = 18, + .param = "[\"" TEST_PATH "/sub1/testfile.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub1\",488]"}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .resultNull = true, .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 18, + .param = "[\"" TEST_PATH "/sub1/testfile.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .resultNull = true, .resultInt = LIBSSH2_ERROR_EAGAIN, .sleep = 5, + .param = "[\"" TEST_PATH "/sub1/testfile.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_EAGAIN}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(file, storageNewWriteP(storageTest, fileName, .noSyncFile = true), "storageWriteSftpOpen timeout EAGAIN"); + TEST_ERROR_FMT(ioWriteOpen(storageWriteIo(file)), FileOpenError, STORAGE_ERROR_WRITE_OPEN, strZ(fileName)); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageWriteSftpOpen libssh2_sftp_open_ex sftp error"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .resultNull = true, .sleep = 18, + .param = "[\"" TEST_PATH "/sub1/testfile.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub1\",488]"}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .resultNull = true, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL, .sleep = 18, + .param = "[\"" TEST_PATH "/sub1/testfile.pgbackrest.tmp\",26,416,0]"}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(file, storageNewWriteP(storageTest, fileName, .noSyncFile = true), "storageWriteSftpOpen sftp error"); + TEST_ERROR_FMT( + ioWriteOpen(storageWriteIo(file)), FileOpenError, + STORAGE_ERROR_WRITE_OPEN ": libssh2 error [-31]: sftp error [3]", strZ(fileName)); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + } + + // ***************************************************************************************************************************** + if (testBegin("StorageRead")) + { + Storage *storageTest = NULL; + StorageRead *file = NULL; + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + // Ignore missing - file with no permission to read + // New read file, check ignore missing, check name require no libssh2 calls + // Permission denied + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/noperm/noperm\",1,0,0]", .resultNull = true, + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL, .sleep = 18}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/noperm/noperm\",1,0,0]", .resultNull = true, + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL, .sleep = 8}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + // File missing + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.file\",1,0,0]", .resultNull = true, + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL, .sleep = 18}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.file\",1,0,0]", .resultNull = true, + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL, .sleep = 8}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + // Ignore missing + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.file\",1,0,0]", .resultNull = true, + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL, .sleep = 18}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.file\",1,0,0]", .resultNull = true, + .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL, .sleep = 8}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("ignore missing - file with no permission to read"); + + // Mimic creation of /noperm/noperm + TEST_ASSIGN(file, storageNewReadP(storageTest, fileNoPerm, .ignoreMissing = true), "new read file"); + TEST_RESULT_BOOL(storageReadIgnoreMissing(file), true, "check ignore missing"); + TEST_RESULT_STR(storageReadName(file), fileNoPerm, "check name"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("permission denied"); + + TEST_ASSIGN(file, storageNewReadP(storageTest, fileNoPerm), "new no perm read file"); + TEST_ERROR_FMT(ioReadOpen(storageReadIo(file)), FileOpenError, STORAGE_ERROR_READ_OPEN, strZ(fileNoPerm)); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("file missing"); + + const String *fileName = STRDEF(TEST_PATH "/test.file"); + + TEST_ASSIGN(file, storageNewReadP(storageTest, fileName), "new missing read file"); + TEST_ERROR_FMT(ioReadOpen(storageReadIo(file)), FileMissingError, STORAGE_ERROR_READ_MISSING, strZ(fileName)); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("ignore missing"); + + TEST_ASSIGN(file, storageNewReadP(storageTest, fileName, .ignoreMissing = true), "new missing read file"); + TEST_RESULT_BOOL(ioReadOpen(storageReadIo(file)), false, "missing file ignored"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("incremental load, storageReadSftp(), storageReadSftpEof()"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + // Open file + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/test.file\",1,0,0]", + .resultInt = LIBSSH2_ERROR_NONE}, + // Check file name, check file type, check offset, check limit require no libssh2 calls + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = 2, .readBuffer = STRDEF("TE")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = 2, .readBuffer = STRDEF("ST")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = 2, .readBuffer = STRDEF("FI")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = 2, .readBuffer = STRDEF("LE")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[2]", .resultInt = 1, .readBuffer = STRDEF("\n")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[1]", .resultInt = 0}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + FSLASH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 5000, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + Buffer *buffer = bufNew(0); + Buffer *outBuffer = bufNew(2); + const Buffer *expectedBuffer = BUFSTRDEF("TESTFILE\n"); + + MEM_CONTEXT_TEMP_BEGIN() + { + TEST_ASSIGN( + file, + storageReadMove(storageNewReadP(storageTest, fileName, .limit = VARUINT64(44)), memContextPrior()), + "new read file"); + } + MEM_CONTEXT_TEMP_END(); + + TEST_RESULT_BOOL(ioReadOpen(storageReadIo(file)), true, "open file"); + TEST_RESULT_STR(storageReadName(file), fileName, "check file name"); + TEST_RESULT_UINT(storageReadType(file), STORAGE_SFTP_TYPE, "check file type"); + TEST_RESULT_UINT(storageReadOffset(file), 0, "check offset"); + TEST_RESULT_UINT(varUInt64(storageReadLimit(file)), 44, "check limit"); + + TEST_RESULT_VOID(ioRead(storageReadIo(file), outBuffer), "load data"); + bufCat(buffer, outBuffer); + bufUsedZero(outBuffer); + TEST_RESULT_VOID(ioRead(storageReadIo(file), outBuffer), "load data"); + bufCat(buffer, outBuffer); + bufUsedZero(outBuffer); + TEST_RESULT_VOID(ioRead(storageReadIo(file), outBuffer), "load data"); + bufCat(buffer, outBuffer); + bufUsedZero(outBuffer); + TEST_RESULT_VOID(ioRead(storageReadIo(file), outBuffer), "load data"); + bufCat(buffer, outBuffer); + bufUsedZero(outBuffer); + TEST_RESULT_BOOL(bufEq(buffer, expectedBuffer), false, "check file contents (not all loaded yet)"); + TEST_RESULT_BOOL(storageReadSftpEof((StorageReadSftp *)file->driver), false, "storageReadSftpEof eof false"); + + TEST_RESULT_VOID(ioRead(storageReadIo(file), outBuffer), "load data"); + bufCat(buffer, outBuffer); + bufUsedZero(outBuffer); + + TEST_RESULT_VOID(ioRead(storageReadIo(file), outBuffer), "no data to load"); + TEST_RESULT_UINT(bufUsed(outBuffer), 0, "buffer is empty"); + + TEST_RESULT_VOID(storageReadSftp(file->driver, outBuffer, true), "no data to load from driver either"); + TEST_RESULT_UINT(bufUsed(outBuffer), 0, "buffer is empty"); + + TEST_RESULT_BOOL(bufEq(buffer, expectedBuffer), true, "check file contents (all loaded)"); + + TEST_RESULT_BOOL(ioReadEof(storageReadIo(file)), true, "eof"); + TEST_RESULT_BOOL(ioReadEof(storageReadIo(file)), true, "still eof"); + TEST_RESULT_BOOL(storageReadSftpEof((StorageReadSftp *)file->driver), true, "storageReadSftpEof eof true"); + + TEST_RESULT_VOID(ioReadClose(storageReadIo(file)), "close file"); + + TEST_RESULT_VOID(storageReadFree(storageNewReadP(storageTest, fileName)), "free file"); + + TEST_RESULT_VOID(storageReadMove(NULL, memContextTop()), "move null file"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + } + + // ***************************************************************************************************************************** + if (testBegin("StorageWrite")) + { + Storage *storageTest = NULL; + StorageWrite *file = NULL; + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("check getters"); + + // mimic creation of /noperm/noperm + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN( + file, + storageNewWriteP( + storageTest, fileNoPerm, .modeFile = 0444, .modePath = 0555, .noSyncFile = true, .noSyncPath = true, + .noAtomic = true), + "new write file"); + TEST_RESULT_BOOL(storageWriteAtomic(file), false, "check atomic"); + TEST_RESULT_BOOL(storageWriteCreatePath(file), true, "check create path"); + TEST_RESULT_INT(storageWriteModeFile(file), 0444, "check mode file"); + TEST_RESULT_INT(storageWriteModePath(file), 0555, "check mode path"); + TEST_RESULT_STR(storageWriteName(file), fileNoPerm, "check name"); + TEST_RESULT_BOOL(storageWriteSyncPath(file), false, "check sync path"); + TEST_RESULT_BOOL(storageWriteSyncFile(file), false, "check sync file"); + TEST_RESULT_BOOL(storageWriteTruncate(file), true, "file will be truncated"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("permission denied"); + + // mimic creation of /noperm/noperm + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = 0}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = 0}, + HOSTKEY_HASH_ENTRY(), + {.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX, + .param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_CSTR "\",\"" KEYPRIV_CSTR "\",null]", + .resultInt = 0}, + {.function = HRNLIBSSH2_SFTP_INIT}, + // Permission denied + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/noperm/noperm\",26,416,0]", .resultNull = true, + .sleep = 18}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/noperm/noperm\",26,416,0]", .resultNull = true, + .sleep = 18, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_PERMISSION_DENIED}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, +#ifdef LIBSSH2_HOSTKEY_HASH_SHA256 + hashTypeSha256, +#else + hashTypeSha1, +#endif + .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(file, storageNewWriteP(storageTest, fileNoPerm, .noAtomic = true), "new write file"); + TEST_ERROR_FMT( + ioWriteOpen(storageWriteIo(file)), + FileOpenError, STORAGE_ERROR_WRITE_OPEN ": libssh2 error [-31]: sftp error [3]", strZ(fileNoPerm)); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("file missing"); + + const String *fileName = STRDEF(TEST_PATH "/sub1/test.file"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = 0}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = 0}, + HOSTKEY_HASH_ENTRY(), + {.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX, + .param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_CSTR "\",\"" KEYPRIV_CSTR "\",null]", .resultInt = 0}, + {.function = HRNLIBSSH2_SFTP_INIT}, + // Missing + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/sub1/test.file\",26,416,0]", .resultNull = true, + .sleep = 18}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/sub1/test.file\",26,416,0]", .resultNull = true, + .sleep = 18, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + {.function = HRNLIBSSH2_SFTP_MKDIR_EX, .param = "[\"" TEST_PATH "/sub1\",488]"}, + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/sub1/test.file\",26,416,0]", .resultNull = true, + .sleep = 18, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SESSION_LAST_ERRNO, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_NO_SUCH_FILE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, +#ifdef LIBSSH2_HOSTKEY_HASH_SHA256 + hashTypeSha256, +#else + hashTypeSha1, +#endif + .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN(file, storageNewWriteP(storageTest, fileName, .noAtomic = true), "new write file"); + TEST_ERROR_FMT(ioWriteOpen(storageWriteIo(file)), FileMissingError, STORAGE_ERROR_WRITE_MISSING, strZ(fileName)); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("write file success"); + + ioBufferSizeSet(65536); + + const Buffer *buffer = BUFSTRDEF("TESTFILE\n"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + {.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = 0}, + {.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"}, + {.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = 0}, + {.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[1]", .resultZ = "12345678910123456789"}, + {.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX, + .param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_CSTR "\",\"" KEYPRIV_CSTR "\",null]", + .resultInt = 0}, + {.function = HRNLIBSSH2_SFTP_INIT}, + // Open file + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/sub1/test.file.pgbackrest.tmp\",26,416,0]"}, + // Write null and zero size buffers are noops + // Write filled buffer to file + {.function = HRNLIBSSH2_SFTP_WRITE, .param = "[9]", .resultInt = 9}, + // Close file + {.function = HRNLIBSSH2_SFTP_FSYNC, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_RENAME_EX, + .param = "[\"" TEST_PATH "/sub1/test.file.pgbackrest.tmp\",\"" TEST_PATH "/sub1/test.file\",7]", + .resultInt = LIBSSH2_ERROR_NONE}, + // Move context + // Open file + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/sub1/test.file\",1,0,0]"}, + // Read file + {.function = HRNLIBSSH2_SFTP_READ, .param = "[65536]", .resultInt = 9, .readBuffer = STRDEF("TESTFILE\n")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[65527]", .resultInt = 0}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE}, + // Check path mode + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/sub1\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU | LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IXGRP, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID | + LIBSSH2_SFTP_ATTR_SIZE, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // Check file mode + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/sub1/test.file\",1]", + .fileName = STRDEF("test.file"), .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR | LIBSSH2_SFTP_S_IRGRP, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // Remove filename + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"" TEST_PATH "/sub1/test.file\"]", + .resultInt = LIBSSH2_ERROR_NONE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeMd5, .keyPub = KEYPUB, .write = true); + + MEM_CONTEXT_TEMP_BEGIN() + { + TEST_ASSIGN(file, storageWriteMove(storageNewWriteP(storageTest, fileName), memContextPrior()), "new write file"); + } + MEM_CONTEXT_TEMP_END(); + + TEST_RESULT_VOID(ioWriteOpen(storageWriteIo(file)), "open file"); + TEST_RESULT_VOID(ioWrite(storageWriteIo(file), NULL), "write null buffer to file"); + TEST_RESULT_VOID(ioWrite(storageWriteIo(file), bufNew(0)), "write zero buffer to file"); + TEST_RESULT_VOID(ioWrite(storageWriteIo(file), buffer), "write to file"); + TEST_RESULT_VOID(ioWriteClose(storageWriteIo(file)), "close file"); + TEST_RESULT_VOID(storageWriteFree(storageNewWriteP(storageTest, fileName)), "free file"); + TEST_RESULT_VOID(storageWriteMove(NULL, memContextTop()), "move null file"); + + Buffer *expectedBuffer = storageGetP(storageNewReadP(storageTest, fileName)); + TEST_RESULT_BOOL(bufEq(buffer, expectedBuffer), true, "check file contents"); + TEST_RESULT_INT(storageInfoP(storageTest, strPath(fileName)).mode, 0750, "check path mode"); + TEST_RESULT_INT(storageInfoP(storageTest, fileName).mode, 0640, "check file mode"); + + storageRemoveP(storageTest, fileName, .errorOnMissing = true); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("write subpath and file success"); + + fileName = STRDEF(TEST_PATH "/sub2/test.file"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + // Open file + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/sub2/test.file\",26,384,0]"}, + // Write filled buffer to file + {.function = HRNLIBSSH2_SFTP_WRITE, .param = "[9]", .resultInt = 9}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE}, + // Open file + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "/sub2/test.file\",1,0,0]"}, + // Read file + {.function = HRNLIBSSH2_SFTP_READ, .param = "[65536]", .resultInt = 9, .readBuffer = STRDEF("TESTFILE\n")}, + {.function = HRNLIBSSH2_SFTP_READ, .param = "[65527]", .resultInt = 0}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE}, + // Check path mode + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/sub2\",1]", .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFDIR | LIBSSH2_SFTP_S_IRWXU, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID | + LIBSSH2_SFTP_ATTR_SIZE, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // Check file mode + {.function = HRNLIBSSH2_SFTP_STAT_EX, .param = "[\"" TEST_PATH "/sub2/test.file\",1]", + .fileName = STRDEF("test.file"), .resultInt = LIBSSH2_ERROR_NONE, + .attrPerms = LIBSSH2_SFTP_S_IFREG | LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR, + .flags = LIBSSH2_SFTP_ATTR_PERMISSIONS | LIBSSH2_SFTP_ATTR_ACMODTIME | LIBSSH2_SFTP_ATTR_UIDGID, + .mtime = 1656434296, .uid = TEST_USER_ID, .gid = TEST_GROUP_ID}, + // Remove filename + {.function = HRNLIBSSH2_SFTP_UNLINK_EX, .param = "[\"" TEST_PATH "/sub2/test.file\"]", + .resultInt = LIBSSH2_ERROR_NONE}, + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + storageTest = storageSftpNewP( + TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB, .write = true); + + TEST_ASSIGN( + file, + storageNewWriteP( + storageTest, fileName, .modePath = 0700, .modeFile = 0600, .noSyncPath = true, .noSyncFile = true, + .noAtomic = true), + "new write file"); + TEST_RESULT_VOID(ioWriteOpen(storageWriteIo(file)), "open file"); + TEST_RESULT_VOID(ioWrite(storageWriteIo(file), buffer), "write to file"); + TEST_RESULT_VOID(ioWriteClose(storageWriteIo(file)), "close file"); + + expectedBuffer = storageGetP(storageNewReadP(storageTest, fileName)); + TEST_RESULT_BOOL(bufEq(buffer, expectedBuffer), true, "check file contents"); + TEST_RESULT_INT(storageInfoP(storageTest, strPath(fileName)).mode, 0700, "check path mode"); + TEST_RESULT_INT(storageInfoP(storageTest, fileName).mode, 0600, "check file mode"); + + storageRemoveP(storageTest, fileName, .errorOnMissing = true); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + } + + // ***************************************************************************************************************************** + if (testBegin("storageRepo*()")) + { + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + // Load configuration to set repo-path and stanza + StringList *argList = strLstNew(); + hrnCfgArgRawZ(argList, cfgOptStanza, "db"); + hrnCfgArgRawZ(argList, cfgOptPgPath, "/path/to/pg"); + hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostUser, TEST_USER); + hrnCfgArgRawZ(argList, cfgOptRepoType, "sftp"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHost, "localhost"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostKeyHashType, "sha1"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpPrivateKeyFile, KEYPRIV_CSTR); + hrnCfgArgRawZ(argList, cfgOptRepoSftpPublicKeyFile, KEYPUB_CSTR); + HRN_CFG_LOAD(cfgCmdArchiveGet, argList); + + const Storage *storage = NULL; + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageRepo() - cached/not cached"); + + // Set storage helper + static const StorageHelper storageHelperList[] = {STORAGE_SFTP_HELPER, STORAGE_END_HELPER}; + storageHelperInit(storageHelperList); + + TEST_RESULT_PTR(storageHelper.storageRepo, NULL, "repo storage not cached"); + TEST_ASSIGN(storage, storageRepo(), "new storage"); + TEST_RESULT_PTR(storageHelper.storageRepo[0], storage, "repo storage cached"); + TEST_RESULT_PTR(storageRepo(), storage, "get cached storage"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageRepo() - confirm settings"); + + TEST_ERROR(storagePathP(storage, STRDEF("/path")), AssertError, "invalid expression ''"); + TEST_ERROR(storageNewWriteP(storage, writeFile), AssertError, "assertion 'this->write' failed"); + + TEST_RESULT_STR_Z(storagePathP(storage, NULL), TEST_PATH, "check base path"); + TEST_RESULT_STR_Z( + storagePathP(storage, STORAGE_REPO_ARCHIVE_STR), TEST_PATH "/archive/db", "check archive path"); + TEST_RESULT_STR_Z( + storagePathP(storage, STRDEF(STORAGE_REPO_ARCHIVE "/simple")), TEST_PATH "/archive/db/simple", + "check simple path"); + TEST_RESULT_STR_Z( + storagePathP(storage, STRDEF(STORAGE_REPO_ARCHIVE "/9.4-1/700000007000000070000000")), + TEST_PATH "/archive/db/9.4-1/7000000070000000/700000007000000070000000", "check segment path"); + TEST_RESULT_STR_Z( + storagePathP(storage, STRDEF(STORAGE_REPO_ARCHIVE "/9.4-1/00000008.history")), + TEST_PATH "/archive/db/9.4-1/00000008.history", "check history path"); + TEST_RESULT_STR_Z( + storagePathP(storage, STRDEF(STORAGE_REPO_ARCHIVE "/9.4-1/000000010000014C0000001A.00000028.backup")), + TEST_PATH "/archive/db/9.4-1/000000010000014C/000000010000014C0000001A.00000028.backup", + "check archive backup path"); + TEST_RESULT_STR_Z(storagePathP(storage, STORAGE_REPO_BACKUP_STR), TEST_PATH "/backup/db", "check backup path"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storage))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageRepo() - helper does not fail when stanza option not set"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + // Change the stanza to NULL with the stanzaInit flag still true, make sure helper does not fail when stanza option not set + argList = strLstNew(); + hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostUser, TEST_USER); + hrnCfgArgRawZ(argList, cfgOptRepoType, "sftp"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHost, "localhost"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostKeyHashType, "sha1"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpPrivateKeyFile, KEYPRIV_CSTR); + hrnCfgArgRawZ(argList, cfgOptRepoSftpPublicKeyFile, KEYPUB_CSTR); + HRN_CFG_LOAD(cfgCmdInfo, argList); + + TEST_ASSIGN(storage, storageRepo(), "new repo storage no stanza"); + TEST_RESULT_STR(storageHelper.stanza, NULL, "stanza NULL"); + + TEST_RESULT_STR_Z( + storagePathP(storage, STORAGE_REPO_ARCHIVE_STR), TEST_PATH "/archive", "check archive path - NULL stanza"); + TEST_RESULT_STR_Z( + storagePathP(storage, STRDEF(STORAGE_REPO_ARCHIVE "/simple")), TEST_PATH "/archive/simple", + "check simple archive path - NULL stanza"); + TEST_RESULT_STR_Z(storagePathP(storage, STORAGE_REPO_BACKUP_STR), TEST_PATH "/backup", "check backup path - NULL stanza"); + TEST_RESULT_STR_Z( + storagePathP(storage, STRDEF(STORAGE_REPO_BACKUP "/simple")), TEST_PATH "/backup/simple", + "check simple backup path - NULL stanza"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storage))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageRepoWrite() - confirm write enabled"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + TEST_RESULT_PTR(storageHelper.storageRepoWrite, NULL, "repo write storage not cached"); + TEST_ASSIGN(storage, storageRepoWrite(), "new write storage"); + TEST_RESULT_PTR(storageHelper.storageRepoWrite[0], storage, "repo write storage cached"); + TEST_RESULT_PTR(storageRepoWrite(), storage, "get cached storage"); + + TEST_RESULT_BOOL(storage->write, true, "get write enabled"); + TEST_RESULT_UINT(storageType(storage), storage->pub.type, "check type"); + TEST_RESULT_STR_Z(strIdToStr(storageType(storage)), "sftp", "storage type is sftp"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storage))); + } + + // ***************************************************************************************************************************** + if (testBegin("storageRepoGet() and StorageDriverCifs")) + { + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + HRNLIBSSH2_MACRO_SHUTDOWN() + }); + + // Load configuration + StringList *argList = strLstNew(); + hrnCfgArgRawZ(argList, cfgOptStanza, "test"); + hrnCfgArgRawZ(argList, cfgOptPgPath, "/path/to/pg"); + hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostUser, TEST_USER); + hrnCfgArgRawZ(argList, cfgOptRepoType, "sftp"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHost, "localhost"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpHostKeyHashType, "sha1"); + hrnCfgArgRawZ(argList, cfgOptRepoSftpPrivateKeyFile, KEYPRIV_CSTR); + hrnCfgArgRawZ(argList, cfgOptRepoSftpPublicKeyFile, KEYPUB_CSTR); + HRN_CFG_LOAD(cfgCmdArchiveGet, argList); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("error on invalid storage type"); + + static const StorageHelper storageHelperListError[] = {{.type = STORAGE_CIFS_TYPE}, STORAGE_END_HELPER}; + storageHelperInit(storageHelperListError); + + TEST_ERROR(storageRepoGet(0, true), AssertError, "invalid storage type"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storage configuration"); + + // Set storage helper + static const StorageHelper storageHelperList[] = {STORAGE_SFTP_HELPER, STORAGE_END_HELPER}; + storageHelperInit(storageHelperList); + + const Storage *storage = NULL; + TEST_ASSIGN(storage, storageRepoGet(0, true), "get sftp repo storage"); + TEST_RESULT_UINT(storageType(storage), STORAGE_SFTP_TYPE, "check storage type"); + TEST_RESULT_BOOL(storageFeature(storage, storageFeaturePath), true, "check path feature"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("write object path sync false"); + + // Create a FileWrite object with path sync enabled and ensure that path sync is false in the write object + StorageWrite *file = NULL; + TEST_ASSIGN(file, storageNewWriteP(storage, STRDEF("somefile"), .noSyncPath = false), "new file write"); + + TEST_RESULT_BOOL(storageWriteSyncPath(file), false, "path sync is disabled"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("path sync result is noop"); + + // Test the path sync function -- pass a bogus path to ensure that this is a noop + TEST_RESULT_VOID(storagePathSyncP(storage, STRDEF(BOGUS_STR)), "path sync is a noop"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storage))); + } + + // ***************************************************************************************************************************** + if (testBegin("storageSftpLibSsh2SessionFreeResource()")) + { + Storage *storageTest = NULL; + StorageSftp *storageSftp = NULL; + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageSftpLibSsh2SessionFreeResource() sftpHandle, sftpSession, session not NULL, branch tests, EAGAIN"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "\",1,0,1]"}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_SHUTDOWN, .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SFTP_SHUTDOWN, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_DISCONNECT_EX, .param = "[11,\"pgbackrest instance shutdown\",\"\"]", + .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SESSION_DISCONNECT_EX, .param = "[11,\"pgbackrest instance shutdown\",\"\"]", + .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_FREE, .resultInt = LIBSSH2_ERROR_EAGAIN}, + {.function = HRNLIBSSH2_SESSION_FREE, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE}, + HRNLIBSSH2_MACRO_SHUTDOWN(), + }); + + TEST_ASSIGN( + storageTest, + storageSftpNewP(TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB), + "new storage (defaults)"); + TEST_ASSIGN(storageSftp, storageDriver(storageTest), "assign storage"); + + // Populate sftpHandle, NULL sftpSession and session + storageSftp->sftpHandle = libssh2_sftp_open_ex(storageSftp->sftpSession, TEST_PATH, 25, 1, 0, 1); + + TEST_RESULT_VOID(storageSftpLibSsh2SessionFreeResource(storageSftp), "freeResource not NULL sftpHandle"); + + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageSftpLibSsh2SessionFreeResource() sftpHandle, sftpSession, session all NULL"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = NULL} + }); + + TEST_ASSIGN( + storageTest, + storageSftpNewP(TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB), + "new storage (defaults)"); + TEST_ASSIGN(storageSftp, storageDriver(storageTest), "assign storage"); + + // NULL out sftpSession and session + storageSftp->sftpSession = NULL; + storageSftp->session = NULL; + + TEST_RESULT_VOID(memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))), "free resource all NULL"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageSftpLibSsh2SessionFreeResource() sftp close handle failure, sftpHandle not NULL, libssh2 error"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "\",1,0,1]"}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_SOCKET_SEND}, + {.function = NULL} + }); + + TEST_ASSIGN( + storageTest, + storageSftpNewP(TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB), + "new storage (defaults)"); + TEST_ASSIGN(storageSftp, storageDriver(storageTest), "assign storage"); + + // Populate sftpHandle, NULL out sftpSession and session + storageSftp->sftpHandle = libssh2_sftp_open_ex(storageSftp->sftpSession, TEST_PATH, 25, 1, 0, 1); + storageSftp->sftpSession = NULL; + storageSftp->session = NULL; + + TEST_ERROR_FMT( + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))), + ServiceError, "failed to free resource sftpHandle: libssh2 errno [-7]"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageSftpLibSsh2SessionFreeResource() sftp close handle failure, sftpHandle not NULL, libssh2 sftp error"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_OPEN_EX, .param = "[\"" TEST_PATH "\",1,0,1]"}, + {.function = HRNLIBSSH2_SFTP_CLOSE_HANDLE, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_CONNECTION_LOST}, + {.function = NULL} + }); + + TEST_ASSIGN( + storageTest, + storageSftpNewP(TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB), + "new storage (defaults)"); + TEST_ASSIGN(storageSftp, storageDriver(storageTest), "assign storage"); + + // Populate sftpHandle, NULL out sftpSession and session + storageSftp->sftpHandle = libssh2_sftp_open_ex(storageSftp->sftpSession, TEST_PATH, 25, 1, 0, 1); + storageSftp->sftpSession = NULL; + storageSftp->session = NULL; + + TEST_ERROR_FMT( + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))), + ServiceError, "failed to free resource sftpHandle: libssh2 errno [-31]: sftp errno [7]"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageSftpLibSsh2SessionFreeResource() sftp shutdown failure libssh2 error"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_SHUTDOWN, .resultInt = LIBSSH2_ERROR_SOCKET_SEND}, + {.function = NULL}, + }); + + TEST_ASSIGN( + storageTest, + storageSftpNewP(TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB), + "new storage (defaults)"); + TEST_ASSIGN(storageSftp, storageDriver(storageTest), "assign storage"); + TEST_ERROR_FMT( + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))), ServiceError, + "failed to free resource sftpSession: libssh2 errno [-7]"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageSftpLibSsh2SessionFreeResource() sftp shutdown failure libssh2 sftp error"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_SHUTDOWN, .resultInt = LIBSSH2_ERROR_SFTP_PROTOCOL}, + {.function = HRNLIBSSH2_SFTP_LAST_ERROR, .resultUInt = LIBSSH2_FX_CONNECTION_LOST}, + {.function = NULL}, + }); + + TEST_ASSIGN( + storageTest, + storageSftpNewP(TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB), + "new storage (defaults)"); + TEST_ASSIGN(storageSftp, storageDriver(storageTest), "assign storage"); + TEST_ERROR_FMT( + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))), ServiceError, + "failed to free resource sftpSession: libssh2 errno [-31]: sftp errno [7]"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageSftpLibSsh2SessionFreeResource() session disconnect failure"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_SHUTDOWN, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_DISCONNECT_EX, .param = "[11,\"pgbackrest instance shutdown\",\"\"]", + .resultInt = LIBSSH2_ERROR_SOCKET_DISCONNECT}, + {.function = NULL}, + }); + + TEST_ASSIGN( + storageTest, + storageSftpNewP(TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB), + "new storage (defaults)"); + TEST_ASSIGN(storageSftp, storageDriver(storageTest), "assign storage"); + TEST_ERROR_FMT( + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))), + ServiceError, "failed to disconnect libssh2 session: libssh2 errno [-13]"); + + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageSftpLibSsh2SessionFreeResource() session free failure"); + + hrnLibSsh2ScriptSet((HrnLibSsh2 []) + { + HRNLIBSSH2_MACRO_STARTUP(), + {.function = HRNLIBSSH2_SFTP_SHUTDOWN, .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_DISCONNECT_EX, .param = "[11,\"pgbackrest instance shutdown\",\"\"]", + .resultInt = LIBSSH2_ERROR_NONE}, + {.function = HRNLIBSSH2_SESSION_FREE, .resultInt = LIBSSH2_ERROR_SOCKET_DISCONNECT}, + {.function = NULL}, + }); + + TEST_ASSIGN( + storageTest, + storageSftpNewP(TEST_PATH_STR, STRDEF("localhost"), 22, TEST_USER_STR, 20, KEYPRIV, hashTypeSha1, .keyPub = KEYPUB), + "new storage (defaults)"); + TEST_ASSIGN(storageSftp, storageDriver(storageTest), "assign storage"); + TEST_ERROR_FMT( + memContextFree(objMemContext((StorageSftp *)storageDriver(storageTest))), ServiceError, + "failed to free libssh2 session: libssh2 errno [-13]"); + } + + // ***************************************************************************************************************************** + if (testBegin("storageSftpEvalLibSsh2Error()")) + { + // ------------------------------------------------------------------------------------------------------------------------- + TEST_TITLE("storageSftpEvalLibSsh2Error()"); + + TEST_ERROR_FMT( + storageSftpEvalLibSsh2Error( + -11, 16, &FileRemoveError, strNewFmt("unable to move '%s' to '%s'", "BOGUS", "NOT BOGUS"), STRDEF("HINT")), + FileRemoveError, + "unable to move 'BOGUS' to 'NOT BOGUS': libssh2 error [-11]\n" + "HINT"); + TEST_ERROR_FMT( + storageSftpEvalLibSsh2Error( + LIBSSH2_ERROR_SFTP_PROTOCOL, 16, &FileRemoveError, + strNewFmt("unable to move '%s' to '%s'", "BOGUS", "NOT BOGUS"), STRDEF("HINT")), + FileRemoveError, + "unable to move 'BOGUS' to 'NOT BOGUS': libssh2 error [-31]: sftp error [16]\n" + "HINT"); + TEST_ERROR_FMT( + storageSftpEvalLibSsh2Error( + LIBSSH2_ERROR_SFTP_PROTOCOL, 16, &FileRemoveError, + strNewFmt("unable to move '%s' to '%s'", "BOGUS", "NOT BOGUS"), NULL), + FileRemoveError, + "unable to move 'BOGUS' to 'NOT BOGUS': libssh2 error [-31]: sftp error [16]"); + TEST_ERROR_FMT( + storageSftpEvalLibSsh2Error( + LIBSSH2_ERROR_SFTP_PROTOCOL, 16, &FileRemoveError, NULL, NULL), + FileRemoveError, + "libssh2 error [-31]: sftp error [16]"); + } + + hrnSckClientOpenShimUninstall(); + FUNCTION_HARNESS_RETURN_VOID(); +} diff --git a/test/src/module/test/testTest.c b/test/src/module/test/testTest.c index 29ba4fc99..a503c5cb4 100644 --- a/test/src/module/test/testTest.c +++ b/test/src/module/test/testTest.c @@ -142,6 +142,7 @@ testRun(void) strReplace(testC, STRDEF("{[C_TEST_USER]}"), STRDEF(TEST_USER)); strReplace(testC, STRDEF("{[C_TEST_USER_ID]}"), STRDEF(TEST_USER_ID_Z)); strReplace(testC, STRDEF("{[C_TEST_USER_ID_Z]}"), STRDEF("\"" TEST_USER_ID_Z "\"")); + strReplace(testC, STRDEF("{[C_TEST_USER_LEN]}"), strNewFmt("%zu", sizeof(TEST_USER) - 1)); // Test definition // ------------------------------------------------------------------------------------------------------------------------- @@ -314,6 +315,7 @@ testRun(void) " lib_openssl,\n" " lib_lz4,\n" " lib_pq,\n" + " lib_ssh2,\n" " lib_xml,\n" " lib_yaml,\n" " lib_z,\n" @@ -429,6 +431,7 @@ testRun(void) " lib_openssl,\n" " lib_lz4,\n" " lib_pq,\n" + " lib_ssh2,\n" " lib_xml,\n" " lib_yaml,\n" " lib_z,\n" @@ -639,6 +642,7 @@ testRun(void) " lib_openssl,\n" " lib_lz4,\n" " lib_pq,\n" + " lib_ssh2,\n" " lib_xml,\n" " lib_yaml,\n" " lib_z,\n" @@ -809,6 +813,7 @@ testRun(void) " lib_openssl,\n" " lib_lz4,\n" " lib_pq,\n" + " lib_ssh2,\n" " lib_xml,\n" " lib_yaml,\n" " lib_z,\n" diff --git a/test/src/test.c b/test/src/test.c index 07bf2ebb1..cee5ef9cb 100644 --- a/test/src/test.c +++ b/test/src/test.c @@ -77,6 +77,7 @@ STRING_EXTERN(HRN_PATH_STR, HRN_PATH); #define TEST_USER "{[C_TEST_USER]}" #define TEST_USER_ID {[C_TEST_USER_ID]} #define TEST_USER_ID_Z "{[C_TEST_USER_ID]}" +#define TEST_USER_LEN "{[C_TEST_USER_LEN]}" #ifdef HRN_FEATURE_STRING STRING_EXTERN(TEST_USER_STR, TEST_USER);