summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorlijia <[email protected]>2024-07-22 10:34:56 +0800
committerlijia <[email protected]>2024-07-22 13:35:54 +0800
commit2ee4770462185daec28e8bb424c8e188e194c996 (patch)
treec4394242f00311d4d7d7c84011672364c851c691
parent6e906a14fee875eff1cac600710f2cedcaaef9b6 (diff)
Initial commitHEADv1.0.1main
-rw-r--r--.gitignore8
-rw-r--r--.gitlab-ci.yml173
-rw-r--r--CMakeLists.txt48
-rw-r--r--autorevision.sh1268
-rw-r--r--bin/ftp.conf3
-rw-r--r--bin/ftp_decoder.inf9
-rw-r--r--ci/get-nprocessors.sh48
-rw-r--r--ci/perpare_pulp3_netrc.sh3
-rw-r--r--ci/travis.sh69
-rw-r--r--cmake/Package.cmake56
-rw-r--r--cmake/Version.cmake54
-rw-r--r--cmake/changelog.sh4
-rw-r--r--cmake/preInstall.sh14
-rw-r--r--cmake/preUninstall.sh6
-rw-r--r--include/ftp_decoder.h38
-rw-r--r--src/CMakeLists.txt15
-rw-r--r--src/ftp_decoder_entry.cpp227
-rw-r--r--src/ftp_decoder_hash.cpp169
-rw-r--r--src/ftp_decoder_hash.h38
-rw-r--r--src/ftp_decoder_inner.h125
-rw-r--r--src/ftp_decoder_proto.cpp566
-rw-r--r--src/ftp_decoder_stat.cpp54
-rw-r--r--src/ftp_decoder_stat.h28
-rw-r--r--src/ftp_decoder_util.cpp163
-rw-r--r--src/ftp_decoder_util.h25
-rw-r--r--src/version.map9
-rw-r--r--test/CMakeLists.txt68
-rw-r--r--test/ftp_decoder_test_plug.cpp117
-rw-r--r--test/ftp_pcap/01-ftp-port-upload-download.pcapbin0 -> 1745959 bytes
-rw-r--r--test/ftp_pcap/01-ftp-port-upload-download_C2S.pcapbin0 -> 722094 bytes
-rw-r--r--test/ftp_pcap/01-ftp-port-upload-download_S2C.pcapbin0 -> 1023889 bytes
-rw-r--r--test/ftp_pcap/02-ftp_v6_1.pcapbin0 -> 12915 bytes
-rw-r--r--test/ftp_pcap/03-ipv6_eport_upload_download.pcapbin0 -> 12210 bytes
-rw-r--r--test/ftp_pcap/04-ftp-banner-no-ftp-characters.pcapbin0 -> 5424 bytes
-rw-r--r--test/ftp_pcap/05-only-ctrl-link.pcapbin0 -> 3083 bytes
-rw-r--r--test/ftp_pcap/06-ftp_pasv-upload-download.pcapbin0 -> 244252 bytes
-rw-r--r--test/ftp_pcap/06-ftp_pasv-upload-download_C2S.pcapbin0 -> 3991 bytes
-rw-r--r--test/ftp_pcap/06-ftp_pasv-upload-download_S2C.pcapbin0 -> 4036 bytes
-rw-r--r--test/ftp_pcap/10-ftp-sdedu.pcapbin0 -> 45153 bytes
-rw-r--r--test/gtest_ftp_decoder.cpp251
-rw-r--r--test/test_result_json/01-ftp-port-upload-download.json66
-rw-r--r--test/test_result_json/01-ftp-port-upload-download_C2S.json28
-rw-r--r--test/test_result_json/01-ftp-port-upload-download_S2C.json8
-rw-r--r--test/test_result_json/02-ftp_v6_1.json57
-rw-r--r--test/test_result_json/03-ipv6_eport_upload_download.json77
-rw-r--r--test/test_result_json/04-ftp-banner-no-ftp-characters.json26
-rw-r--r--test/test_result_json/05-only-ctrl-link.json11
-rw-r--r--test/test_result_json/06-ftp_pasv-upload-download.json65
-rw-r--r--test/test_result_json/06-ftp_pasv-upload-download_C2S.json18
-rw-r--r--test/test_result_json/06-ftp_pasv-upload-download_S2C.json29
-rw-r--r--test/test_result_json/empty.json1
-rw-r--r--test/uthash-2.3.0/include/utarray.h248
-rw-r--r--test/uthash-2.3.0/include/uthash.h1136
-rw-r--r--test/uthash-2.3.0/include/utlist.h1073
-rw-r--r--test/uthash-2.3.0/include/utringbuffer.h108
-rw-r--r--test/uthash-2.3.0/include/utstack.h88
-rw-r--r--test/uthash-2.3.0/include/utstring.h407
-rw-r--r--vendor/CMakeLists.txt23
-rw-r--r--vendor/googletest-release-1.8.0.tar.gzbin0 -> 1281617 bytes
-rw-r--r--vendor/sapp-4.3.56.a47b3b5-1.el8.x86_64.rpmbin0 -> 1153208 bytes
-rw-r--r--vendor/sapp-devel-4.3.56.a47b3b5-1.el8.x86_64.rpmbin0 -> 3248416 bytes
-rw-r--r--vendor/stellar-on-sapp-2.1.1.7875675-1.el8.x86_64.rpmbin0 -> 36124 bytes
62 files changed, 7125 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fe44c80
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+.idea/
+.vscode
+build/
+build7
+build8
+cmake-build*
+.VSCodeCounter
+.cache
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..d12c7c7
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,173 @@
+variables:
+ GIT_STRATEGY: "clone"
+ BUILD_IMAGE_CENTOS8: "git.mesalab.cn:7443/mesa_platform/build-env:rockylinux"
+ BUILD_PADDING_PREFIX: /tmp/padding_for_CPACK_RPM_BUILD_SOURCE_DIRS_PREFIX_PREFIX_PREFIX_PREFIX_PREFIX_PREFIX/
+ INSTALL_DEPENDENCY_PLATFORM: sapp-devel libasan systemd-devel libnsl glib2-devel libMESA_field_stat2 libfieldstat3 mrzcpd-corei7 hasp-tools libuuid-devel libbreakpad_mini
+ INSTALL_DEPENDENCY_FRAMEWORK: libMESA_handle_logger-devel libMESA_handle_logger framework_env
+ libMESA_prof_load-devel libMESA_prof_load libbreakpad_mini-devel libMESA_htable-devel libMESA_htable
+ stellar-on-sapp-devel stellar-on-sapp systemd-devel
+ libMESA_jump_layer libcjson-devel libcjson libfieldstat4-devel libfieldstat4
+ INSTALL_PREFIX: "/opt/tsg/"
+ RPM_REPO_PREFIX: stellar
+
+stages:
+- build
+- test
+- package
+
+.build_before_script:
+ before_script:
+ - mkdir -p $BUILD_PADDING_PREFIX/$CI_PROJECT_NAMESPACE/
+ - ln -s $CI_PROJECT_DIR $BUILD_PADDING_PREFIX/$CI_PROJECT_PATH
+ - cd $BUILD_PADDING_PREFIX/$CI_PROJECT_PATH
+ - chmod +x ./ci/travis.sh
+ - yum makecache
+ - yum install -y elfutils-libelf-devel
+
+.build_by_travis_for_centos8:
+ stage: build
+ image: $BUILD_IMAGE_CENTOS8
+ extends: .build_before_script
+ script:
+ - dnf --enablerepo=powertools install -y libmnl-devel
+ - dnf --enablerepo=powertools install -y libnfnetlink-devel
+ - ./ci/travis.sh
+ tags:
+ - share
+
+branch_build_debug_for_centos8:
+ stage: build
+ extends: .build_by_travis_for_centos8
+ variables:
+ BUILD_TYPE: Debug
+ except:
+ - /^develop.*$/i
+ - /^master.*$/i
+ - tags
+
+branch_build_release_for_centos8:
+ stage: build
+ variables:
+ BUILD_TYPE: RelWithDebInfo
+ extends: .build_by_travis_for_centos8
+ except:
+ - /^develop.*$/i
+ - /^master.*$/i
+ - tags
+
+develop_build_debug_for_centos8:
+ stage: build
+ extends: .build_by_travis_for_centos8
+ variables:
+ BUILD_TYPE: Debug
+ PACKAGE: 1
+ UPLOAD_RPM: 1
+ ASAN_OPTION: ADDRESS
+ TESTING_VERSION_BUILD: 1
+ PULP3_REPO_NAME: $RPM_REPO_PREFIX-testing-x86_64.el8
+ PULP3_DIST_NAME: $RPM_REPO_PREFIX-testing-x86_64.el8
+ artifacts:
+ name: "ftp_decoder-$CI_COMMIT_REF_NAME-debug"
+ paths:
+ - build/*.rpm
+ only:
+ - /^develop.*$/i
+ - /^master.*$/i
+
+develop_build_release_for_centos8:
+ stage: build
+ extends: .build_by_travis_for_centos8
+ variables:
+ BUILD_TYPE: RelWithDebInfo
+ PACKAGE: 1
+ UPLOAD_RPM: 1
+ TESTING_VERSION_BUILD: 1
+ PULP3_REPO_NAME: $RPM_REPO_PREFIX-testing-x86_64.el8
+ PULP3_DIST_NAME: $RPM_REPO_PREFIX-testing-x86_64.el8
+ artifacts:
+ name: "ftp_decoder-$CI_COMMIT_REF_NAME-release"
+ paths:
+ - build/*.rpm
+ only:
+ - /^develop.*$/i
+ - /^master.*$/i
+
+release_build_debug_for_centos8:
+ stage: package
+ variables:
+ BUILD_TYPE: Debug
+ PACKAGE: 1
+ UPLOAD_RPM: 1
+ ASAN_OPTION: ADDRESS
+ PULP3_REPO_NAME: $RPM_REPO_PREFIX-stable-x86_64.el8
+ PULP3_DIST_NAME: $RPM_REPO_PREFIX-stable-x86_64.el8
+ extends: .build_by_travis_for_centos8
+ artifacts:
+ name: "ftp_decoder-$CI_COMMIT_REF_NAME-debug"
+ paths:
+ - build/*.rpm
+ only:
+ - tags
+
+release_build_release_for_centos8:
+ stage: package
+ variables:
+ BUILD_TYPE: RelWithDebInfo
+ PACKAGE: 1
+ UPLOAD_RPM: 1
+ UPLOAD_SYMBOL_FILES: 1
+ SYMBOL_TARGET: ftp_decoder
+ PULP3_REPO_NAME: $RPM_REPO_PREFIX-stable-x86_64.el8
+ PULP3_DIST_NAME: $RPM_REPO_PREFIX-stable-x86_64.el8
+ extends: .build_by_travis_for_centos8
+ artifacts:
+ name: "ftp_decoder-$CI_COMMIT_REF_NAME-release"
+ paths:
+ - build/*.rpm
+ only:
+ - tags
+
+###############################################################################
+# test
+###############################################################################
+
+.install_rpm_package: &install_rpm_package
+ - rpm -e sapp || true
+ - rpm -e sapp-devel || true
+ - rpm -e stellar-on-sapp || true
+ - rpm -e stellar-on-sapp-devel || true
+ - rpm -ivh /tmp/sapp.x86_64.rpm --prefix=${INSTALL_PREFIX}/sapp --force --nodeps
+ - rpm -ivh /tmp/sapp-devel.x86_64.rpm --prefix=${INSTALL_PREFIX}/sapp --force --nodeps
+ - rpm -ivh /tmp/stellar-on-sapp.x86_64.rpm --prefix=${INSTALL_PREFIX} --force --nodeps
+ - rpm -qa | grep sapp
+ - rpm -qa | grep stellar-on-sapp
+ - ls -l /opt/MESA/lib && echo "/opt/MESA/lib" >> /etc/ld.so.conf
+
+
+history_version_test:
+ stage: test
+ extends: .build_by_travis_for_centos8
+ script:
+ - yum makecache
+ - ./ci/travis.sh
+ - cp -f ./vendor/stellar-on-sapp*.rpm /tmp/stellar-on-sapp.x86_64.rpm
+ - cp -f ./vendor/sapp-devel*.rpm /tmp/sapp-devel.x86_64.rpm
+ - cp -f ./vendor/sapp-4*.rpm /tmp/sapp.x86_64.rpm
+ - *install_rpm_package
+ - cd build
+ - ctest3 --verbose
+
+latest_version_test:
+ stage: test
+ extends: .build_by_travis_for_centos8
+ script:
+ - yum makecache
+ - ./ci/travis.sh
+ - rm -f stellar-on-sapp*.rpm sapp*.rpm
+ - yumdownloader stellar-on-sapp sapp-devel sapp
+ - cp -f stellar-on-sapp*.rpm /tmp/stellar-on-sapp.x86_64.rpm
+ - cp -f sapp-devel*.rpm /tmp/sapp-devel.x86_64.rpm
+ - cp -f sapp-4*.rpm /tmp/sapp.x86_64.rpm
+ - *install_rpm_package
+ - cd build
+ - ctest3 --verbose \ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..5cbbb11
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,48 @@
+cmake_minimum_required (VERSION 2.8...3.10)
+
+set(lib_name ftp_decoder)
+
+project (${lib_name})
+
+set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
+include(Version)
+
+set(CMAKE_MACOSX_RPATH 0)
+set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -Wall)
+
+#for ASAN
+set(ASAN_OPTION "OFF" CACHE STRING " set asan type chosen by the user, using OFF as default")
+set_property(CACHE ASAN_OPTION PROPERTY STRINGS OFF ADDRESS THREAD)
+message(STATUS "ASAN_OPTION='${ASAN_OPTION}'")
+
+if(ASAN_OPTION MATCHES "ADDRESS")
+ set(CMAKE_C_FLAGS "${CMAKADDRESS} -g -DCMAKE_BUILD_TYPE=Debug -fsanitize=address -fno-omit-frame-pointer")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -DCMAKE_BUILD_TYPE=Debug -fsanitize=address -fno-omit-frame-pointer")
+ set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -lasan")
+ set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -lasan")
+elseif(ASAN_OPTION MATCHES "THREAD")
+ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g -DCMAKE_BUILD_TYPE=Debug -fsanitize=thread -fno-omit-frame-pointer")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -DCMAKE_BUILD_TYPE=Debug -fsanitize=thread -fno-omit-frame-pointer")
+ set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -lasan")
+ set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -lasan")
+endif()
+# end of for ASAN
+
+set(CMAKE_INSTALL_PREFIX /opt/tsg)
+
+include_directories(include)
+include_directories(/opt/MESA/include/MESA/)
+include_directories(/opt/tsg/framework/include/)
+
+add_subdirectory(vendor)
+add_subdirectory(src)
+
+enable_testing()
+add_subdirectory(test)
+
+set(CPACK_RPM_USER_FILELIST "%config(noreplace) ${CMAKE_INSTALL_PREFIX}/sapp/conf/ftp_decoder/ftp.conf")
+
+install(FILES bin/ftp.conf DESTINATION ${CMAKE_INSTALL_PREFIX}/sapp/conf/ftp_decoder COMPONENT PROFILE)
+install(FILES include/ftp_decoder.h DESTINATION ${CMAKE_INSTALL_PREFIX}/framework/include/ftp_decoder COMPONENT HEADER)
+
+include(Package)
diff --git a/autorevision.sh b/autorevision.sh
new file mode 100644
index 0000000..3baa179
--- /dev/null
+++ b/autorevision.sh
@@ -0,0 +1,1268 @@
+#!/bin/sh
+
+# Copyright (c) 2012 - 2016 dak180 and contributors. See
+# https://opensource.org/licenses/mit-license.php or the included
+# COPYING.md for licence terms.
+#
+# autorevision - extracts metadata about the head version from your
+# repository.
+
+# Usage message.
+arUsage() {
+ cat > "/dev/stderr" << EOF
+usage: autorevision {-t output-type | -s symbol} [-o cache-file [-f] ] [-V]
+ Options include:
+ -t output-type = specify output type
+ -s symbol = specify symbol output
+ -o cache-file = specify cache file location
+ -f = force the use of cache data
+ -U = check for untracked files in svn
+ -V = emit version and exit
+ -? = help message
+
+The following are valid output types:
+ clojure = clojure file
+ c = C/C++ file
+ h = Header for use with c/c++
+ hpp = Alternate C++ header strings with namespace
+ ini = INI file
+ java = Java file
+ javaprop = Java properties file
+ js = javascript file
+ json = JSON file
+ lua = Lua file
+ m4 = m4 file
+ matlab = matlab file
+ octave = octave file
+ php = PHP file
+ pl = Perl file
+ py = Python file
+ rpm = rpm file
+ scheme = scheme file
+ sh = Bash sytax
+ swift = Swift file
+ tex = (La)TeX file
+ xcode = Header useful for populating info.plist files
+ cmake = CMake file
+
+
+The following are valid symbols:
+ VCS_TYPE
+ VCS_BASENAME
+ VCS_UUID
+ VCS_NUM
+ VCS_DATE
+ VCS_BRANCH
+ VCS_TAG
+ VCS_TICK
+ VCS_EXTRA
+ VCS_FULL_HASH
+ VCS_SHORT_HASH
+ VCS_WC_MODIFIED
+ VCS_ACTION_STAMP
+EOF
+ exit 1
+}
+
+# Config
+ARVERSION="&&ARVERSION&&"
+TARGETFILE="/dev/stdout"
+while getopts ":t:o:s:VfU" OPTION; do
+ case "${OPTION}" in
+ t)
+ AFILETYPE="${OPTARG}"
+ ;;
+ o)
+ CACHEFILE="${OPTARG}"
+ ;;
+ f)
+ CACHEFORCE="1"
+ ;;
+ s)
+ VAROUT="${OPTARG}"
+ ;;
+ U)
+ UNTRACKEDFILES="1"
+ ;;
+ V)
+ echo "autorevision ${ARVERSION}"
+ exit 0
+ ;;
+ ?)
+ # If an unknown flag is used (or -?):
+ arUsage
+ ;;
+ esac
+done
+
+if [ ! -z "${VAROUT}" ] && [ ! -z "${AFILETYPE}" ]; then
+ # If both -s and -t are specified:
+ echo "error: Improper argument combination." 1>&2
+ exit 1
+elif [ -z "${VAROUT}" ] && [ -z "${AFILETYPE}" ]; then
+ # If neither -s or -t are specified:
+ arUsage
+elif [ -z "${CACHEFILE}" ] && [ "${CACHEFORCE}" = "1" ]; then
+ # If -f is specified without -o:
+ arUsage
+elif [ ! -f "${CACHEFILE}" ] && [ "${CACHEFORCE}" = "1" ]; then
+ # If we are forced to use the cache but it does not exist.
+ echo "error: Cache forced but no cache found." 1>&2
+ exit 1
+fi
+
+# Make sure that the path we are given is one we can source
+# (dash, we are looking at you).
+if [ ! -z "${CACHEFILE}" ] && ! echo "${CACHEFILE}" | grep -q '^\.*/'; then
+ CACHEFILE="./${CACHEFILE}"
+fi
+
+GENERATED_HEADER="Generated by autorevision - do not hand-hack!"
+
+# Functions to extract data from different repo types.
+# For git repos
+# shellcheck disable=SC2039,SC2164,SC2155
+gitRepo() {
+ local oldPath="${PWD}"
+
+ cd "$(git rev-parse --show-toplevel)"
+
+ VCS_TYPE="git"
+
+ VCS_BASENAME="$(basename "${PWD}")"
+
+ VCS_UUID="$(git rev-list --max-parents=0 --date-order --reverse HEAD 2>/dev/null | sed -n 1p)"
+ if [ -z "${VCS_UUID}" ]; then
+ VCS_UUID="$(git rev-list --topo-order HEAD | tail -n 1)"
+ fi
+
+ # Is the working copy clean?
+ test -z "$(git status --untracked-files=normal --porcelain)"
+ VCS_WC_MODIFIED="${?}"
+
+ # Enumeration of changesets
+ VCS_NUM="$(git rev-list --count HEAD 2>/dev/null)"
+ if [ -z "${VCS_NUM}" ]; then
+ echo "warning: Counting the number of revisions may be slower due to an outdated git version less than 1.7.2.3. If something breaks, please update it." 1>&2
+ VCS_NUM="$(git rev-list HEAD | wc -l)"
+ fi
+
+ # This may be a git-svn remote. If so, report the Subversion revision.
+ if [ -z "$(git config svn-remote.svn.url 2>/dev/null)" ]; then
+ # The full revision hash
+ VCS_FULL_HASH="$(git rev-parse HEAD)"
+
+ # The short hash
+ VCS_SHORT_HASH="$(echo "${VCS_FULL_HASH}" | cut -b 1-7)"
+ else
+ # The git-svn revision number
+ VCS_FULL_HASH="$(git svn find-rev HEAD)"
+ VCS_SHORT_HASH="${VCS_FULL_HASH}"
+ fi
+
+ # Current branch
+ VCS_BRANCH="$(git rev-parse --symbolic-full-name --verify "$(git name-rev --name-only --no-undefined HEAD 2>/dev/null)" 2>/dev/null | sed -e 's:refs/heads/::' | sed -e 's:refs/::')"
+
+ # Cache the description
+ local DESCRIPTION="$(git describe --long --tags 2>/dev/null)"
+
+ # Current or last tag ancestor (empty if no tags)
+ VCS_TAG="$(echo "${DESCRIPTION}" | sed -e "s:-g${VCS_SHORT_HASH}\$::" -e 's:-[0-9]*$::')"
+
+ # Distance to last tag or an alias of VCS_NUM if there is no tag
+ if [ ! -z "${DESCRIPTION}" ]; then
+ VCS_TICK="$(echo "${DESCRIPTION}" | sed -e "s:${VCS_TAG}-::" -e "s:-g${VCS_SHORT_HASH}::")"
+ else
+ VCS_TICK="${VCS_NUM}"
+ fi
+
+ # Date of the current commit
+ VCS_DATE="$(TZ=UTC git show -s --date=iso-strict-local --pretty=format:%ad | sed -e 's|+00:00|Z|')"
+ if [ -z "${VCS_DATE}" ]; then
+ echo "warning: Action stamps require git version 2.7+." 1>&2
+ VCS_DATE="$(git log -1 --pretty=format:%ci | sed -e 's: :T:' -e 's: ::' -e 's|+00:00|Z|')"
+ local ASdis="1"
+ fi
+
+ # Action Stamp
+ if [ -z "${ASdis}" ]; then
+ VCS_ACTION_STAMP="${VCS_DATE}!$(git show -s --pretty=format:%cE)"
+ else
+ VCS_ACTION_STAMP=""
+ fi
+
+ cd "${oldPath}"
+}
+
+# For hg repos
+# shellcheck disable=SC2039,SC2164
+hgRepo() {
+ local oldPath="${PWD}"
+
+ cd "$(hg root)"
+
+ VCS_TYPE="hg"
+
+ VCS_BASENAME="$(basename "${PWD}")"
+
+ VCS_UUID="$(hg log -r "0" -l 1 --template '{node}\n')"
+
+ # Is the working copy clean?
+ test -z "$(hg status -duram)"
+ VCS_WC_MODIFIED="${?}"
+
+ # Enumeration of changesets
+ VCS_NUM="$(hg id -n | tr -d '+')"
+
+ # The full revision hash
+ VCS_FULL_HASH="$(hg log -r "${VCS_NUM}" -l 1 --template '{node}\n')"
+
+ # The short hash
+ VCS_SHORT_HASH="$(hg id -i | tr -d '+')"
+
+ # Current bookmark (bookmarks are roughly equivalent to git's branches)
+ # or branch if no bookmark
+ VCS_BRANCH="$(hg id -B | cut -d ' ' -f 1)"
+ # Fall back to the branch if there are no bookmarks
+ if [ -z "${VCS_BRANCH}" ]; then
+ VCS_BRANCH="$(hg id -b)"
+ fi
+
+ # Current or last tag ancestor (excluding auto tags, empty if no tags)
+ VCS_TAG="$(hg log -r "${VCS_NUM}" -l 1 --template '{latesttag}\n' 2>/dev/null | sed -e 's:qtip::' -e 's:tip::' -e 's:qbase::' -e 's:qparent::' -e "s:$(hg --config 'extensions.color=' --config 'extensions.mq=' --color never qtop 2>/dev/null)::" | cut -d ' ' -f 1)"
+
+ # Distance to last tag or an alias of VCS_NUM if there is no tag
+ if [ ! -z "${VCS_TAG}" ]; then
+ VCS_TICK="$(hg log -r "${VCS_NUM}" -l 1 --template '{latesttagdistance}\n' 2>/dev/null)"
+ else
+ VCS_TICK="${VCS_NUM}"
+ fi
+
+ # Date of the current commit
+ VCS_DATE="$(hg log -r "${VCS_NUM}" -l 1 --template '{date|isodatesec}\n' 2>/dev/null | sed -e 's: :T:' -e 's: ::' -e 's|+00:00|Z|')"
+
+ # Action Stamp
+ VCS_ACTION_STAMP="$(TZ=UTC hg log -r "${VCS_NUM}" -l 1 --template '{date|localdate|rfc3339date}\n' 2>/dev/null | sed -e 's|+00:00|Z|')!$(hg log -r "${VCS_NUM}" -l 1 --template '{author|email}\n' 2>/dev/null)"
+
+ cd "${oldPath}"
+}
+
+# For bzr repos
+# shellcheck disable=SC2039,SC2164
+bzrRepo() {
+ local oldPath="${PWD}"
+
+ cd "$(bzr root)"
+
+ VCS_TYPE="bzr"
+
+ VCS_BASENAME="$(basename "${PWD}")"
+
+ # Currently unimplemented because more investigation is needed.
+ VCS_UUID=""
+
+ # Is the working copy clean?
+ bzr version-info --custom --template='{clean}\n' | grep -q '1'
+ VCS_WC_MODIFIED="${?}"
+
+ # Enumeration of changesets
+ VCS_NUM="$(bzr revno)"
+
+ # The full revision hash
+ VCS_FULL_HASH="$(bzr version-info --custom --template='{revision_id}\n')"
+
+ # The short hash
+ VCS_SHORT_HASH="${VCS_NUM}"
+
+ # Nick of the current branch
+ VCS_BRANCH="$(bzr nick)"
+
+ # Current or last tag ancestor (excluding auto tags, empty if no tags)
+ VCS_TAG="$(bzr tags --sort=time | sed '/?$/d' | tail -n1 | cut -d ' ' -f1)"
+
+ # Distance to last tag or an alias of VCS_NUM if there is no tag
+ if [ ! -z "${VCS_TAG}" ]; then
+ VCS_TICK="$(bzr log --line -r "tag:${VCS_TAG}.." | tail -n +2 | wc -l | sed -e 's:^ *::')"
+ else
+ VCS_TICK="${VCS_NUM}"
+ fi
+
+ # Date of the current commit
+ VCS_DATE="$(bzr version-info --custom --template='{date}\n' | sed -e 's: :T:' -e 's: ::')"
+
+ # Action Stamp
+ # Currently unimplemented because more investigation is needed.
+ VCS_ACTION_STAMP=""
+
+ cd "${oldPath}"
+}
+
+# For svn repos
+# shellcheck disable=SC2039,SC2164,SC2155
+svnRepo() {
+ local oldPath="${PWD}"
+
+ VCS_TYPE="svn"
+
+ case "${PWD}" in
+ /*trunk*|/*branches*|/*tags*)
+ local fn="${PWD}"
+ while [ "$(basename "${fn}")" != 'trunk' ] && [ "$(basename "${fn}")" != 'branches' ] && [ "$(basename "${fn}")" != 'tags' ] && [ "$(basename "${fn}")" != '/' ]; do
+ local fn="$(dirname "${fn}")"
+ done
+ local fn="$(dirname "${fn}")"
+ if [ "${fn}" = '/' ]; then
+ VCS_BASENAME="$(basename "${PWD}")"
+ else
+ VCS_BASENAME="$(basename "${fn}")"
+ fi
+ ;;
+ *) VCS_BASENAME="$(basename "${PWD}")" ;;
+ esac
+
+ VCS_UUID="$(svn info --xml | sed -n -e 's:<uuid>::' -e 's:</uuid>::p')"
+
+ # Cache svnversion output
+ local SVNVERSION="$(svnversion)"
+
+ # Is the working copy clean?
+ echo "${SVNVERSION}" | grep -q "M"
+ case "${?}" in
+ 0)
+ VCS_WC_MODIFIED="1"
+ ;;
+ 1)
+ if [ ! -z "${UNTRACKEDFILES}" ]; then
+ # `svnversion` does not detect untracked files and `svn status` is really slow, so only run it if we really have to.
+ if [ -z "$(svn status)" ]; then
+ VCS_WC_MODIFIED="0"
+ else
+ VCS_WC_MODIFIED="1"
+ fi
+ else
+ VCS_WC_MODIFIED="0"
+ fi
+ ;;
+ esac
+
+ # Enumeration of changesets
+ VCS_NUM="$(echo "${SVNVERSION}" | cut -d : -f 1 | sed -e 's:M::' -e 's:S::' -e 's:P::')"
+
+ # The full revision hash
+ VCS_FULL_HASH="${SVNVERSION}"
+
+ # The short hash
+ VCS_SHORT_HASH="${VCS_NUM}"
+
+ # Current branch
+ case "${PWD}" in
+ /*trunk*|/*branches*|/*tags*)
+ local lastbase=""
+ local fn="${PWD}"
+ while :
+ do
+ base="$(basename "${fn}")"
+ if [ "${base}" = 'trunk' ]; then
+ VCS_BRANCH='trunk'
+ break
+ elif [ "${base}" = 'branches' ] || [ "${base}" = 'tags' ]; then
+ VCS_BRANCH="${lastbase}"
+ break
+ elif [ "${base}" = '/' ]; then
+ VCS_BRANCH=""
+ break
+ fi
+ local lastbase="${base}"
+ local fn="$(dirname "${fn}")"
+ done
+ ;;
+ *) VCS_BRANCH="" ;;
+ esac
+
+ # Current or last tag ancestor (empty if no tags). But "current
+ # tag" can't be extracted reliably because Subversion doesn't
+ # have tags the way other VCSes do.
+ VCS_TAG=""
+ VCS_TICK=""
+
+ # Date of the current commit
+ VCS_DATE="$(svn info --xml | sed -n -e 's:<date>::' -e 's:</date>::p')"
+
+ # Action Stamp
+ VCS_ACTION_STAMP="${VCS_DATE}!$(svn log --xml -l 1 -r "${VCS_SHORT_HASH}" | sed -n -e 's:<author>::' -e 's:</author>::p')"
+
+ cd "${oldPath}"
+}
+
+
+# Functions to output data in different formats.
+# For bash output
+shOutput() {
+ cat > "${TARGETFILE}" << EOF
+# ${GENERATED_HEADER}
+
+VCS_TYPE="${VCS_TYPE}"
+VCS_BASENAME="${VCS_BASENAME}"
+VCS_UUID="${VCS_UUID}"
+VCS_NUM="${VCS_NUM}"
+VCS_DATE="${VCS_DATE}"
+VCS_BRANCH="${VCS_BRANCH}"
+VCS_TAG="${VCS_TAG}"
+VCS_TICK="${VCS_TICK}"
+VCS_EXTRA="${VCS_EXTRA}"
+
+VCS_ACTION_STAMP="${VCS_ACTION_STAMP}"
+VCS_FULL_HASH="${VCS_FULL_HASH}"
+VCS_SHORT_HASH="${VCS_SHORT_HASH}"
+
+VCS_WC_MODIFIED="${VCS_WC_MODIFIED}"
+
+# end
+EOF
+}
+
+# For source C output
+cOutput() {
+ cat > "${TARGETFILE}" << EOF
+/* ${GENERATED_HEADER} */
+
+const char *VCS_TYPE = "${VCS_TYPE}";
+const char *VCS_BASENAME = "${VCS_BASENAME}";
+const char *VCS_UUID = "${VCS_UUID}";
+const int VCS_NUM = ${VCS_NUM};
+const char *VCS_DATE = "${VCS_DATE}";
+const char *VCS_BRANCH = "${VCS_BRANCH}";
+const char *VCS_TAG = "${VCS_TAG}";
+const int VCS_TICK = ${VCS_TICK};
+const char *VCS_EXTRA = "${VCS_EXTRA}";
+
+const char *VCS_ACTION_STAMP = "${VCS_ACTION_STAMP}";
+const char *VCS_FULL_HASH = "${VCS_FULL_HASH}";
+const char *VCS_SHORT_HASH = "${VCS_SHORT_HASH}";
+
+const int VCS_WC_MODIFIED = ${VCS_WC_MODIFIED};
+
+/* end */
+EOF
+}
+
+# For header output
+hOutput() {
+ cat > "${TARGETFILE}" << EOF
+/* ${GENERATED_HEADER} */
+#ifndef AUTOREVISION_H
+#define AUTOREVISION_H
+
+#define VCS_TYPE "${VCS_TYPE}"
+#define VCS_BASENAME "${VCS_BASENAME}"
+#define VCS_UUID "${VCS_UUID}"
+#define VCS_NUM ${VCS_NUM}
+#define VCS_DATE "${VCS_DATE}"
+#define VCS_BRANCH "${VCS_BRANCH}"
+#define VCS_TAG "${VCS_TAG}"
+#define VCS_TICK ${VCS_TICK}
+#define VCS_EXTRA "${VCS_EXTRA}"
+
+#define VCS_ACTION_STAMP "${VCS_ACTION_STAMP}"
+#define VCS_FULL_HASH "${VCS_FULL_HASH}"
+#define VCS_SHORT_HASH "${VCS_SHORT_HASH}"
+
+#define VCS_WC_MODIFIED ${VCS_WC_MODIFIED}
+
+#endif
+
+/* end */
+EOF
+}
+
+# A header output for use with xcode to populate info.plist strings
+xcodeOutput() {
+ cat > "${TARGETFILE}" << EOF
+/* ${GENERATED_HEADER} */
+#ifndef AUTOREVISION_H
+#define AUTOREVISION_H
+
+#define VCS_TYPE ${VCS_TYPE}
+#define VCS_BASENAME ${VCS_BASENAME}
+#define VCS_UUID ${VCS_UUID}
+#define VCS_NUM ${VCS_NUM}
+#define VCS_DATE ${VCS_DATE}
+#define VCS_BRANCH ${VCS_BRANCH}
+#define VCS_TAG ${VCS_TAG}
+#define VCS_TICK ${VCS_TICK}
+#define VCS_EXTRA ${VCS_EXTRA}
+
+#define VCS_ACTION_STAMP ${VCS_ACTION_STAMP}
+#define VCS_FULL_HASH ${VCS_FULL_HASH}
+#define VCS_SHORT_HASH ${VCS_SHORT_HASH}
+
+#define VCS_WC_MODIFIED ${VCS_WC_MODIFIED}
+
+#endif
+
+/* end */
+EOF
+}
+
+# For Swift output
+swiftOutput() {
+ case "${VCS_WC_MODIFIED}" in
+ 0) VCS_WC_MODIFIED="false" ;;
+ 1) VCS_WC_MODIFIED="true" ;;
+ esac
+ # For values that may not exist depending on the type of repo we
+ # have read from, set them to `nil` when they are empty.
+ if [ -z "${VCS_UUID}" ]; then
+ VCS_UUID="nil"
+ else
+ VCS_UUID="\"${VCS_UUID}\""
+ fi
+ if [ -z "${VCS_TAG}" ]; then
+ VCS_TAG="nil"
+ else
+ VCS_TAG="\"${VCS_TAG}\""
+ fi
+ : "${VCS_TICK:="nil"}"
+ if [ -z "${VCS_EXTRA}" ]; then
+ VCS_EXTRA="nil"
+ else
+ VCS_EXTRA="\"${VCS_EXTRA}\""
+ fi
+ if [ -z "${VCS_ACTION_STAMP}" ]; then
+ VCS_ACTION_STAMP="nil"
+ else
+ VCS_ACTION_STAMP="\"${VCS_ACTION_STAMP}\""
+ fi
+ cat > "${TARGETFILE}" << EOF
+/* ${GENERATED_HEADER} */
+
+let VCS_TYPE = "${VCS_TYPE}"
+let VCS_BASENAME = "${VCS_BASENAME}"
+let VCS_UUID: String? = ${VCS_UUID}
+let VCS_NUM: Int = ${VCS_NUM}
+let VCS_DATE = "${VCS_DATE}"
+let VCS_BRANCH: String = "${VCS_BRANCH}"
+let VCS_TAG: String? = ${VCS_TAG}
+let VCS_TICK: Int? = ${VCS_TICK}
+let VCS_EXTRA: String? = ${VCS_EXTRA}
+
+let VCS_ACTION_STAMP: String? = ${VCS_ACTION_STAMP}
+let VCS_FULL_HASH: String = "${VCS_FULL_HASH}"
+let VCS_SHORT_HASH: String = "${VCS_SHORT_HASH}"
+
+let VCS_WC_MODIFIED: Bool = ${VCS_WC_MODIFIED}
+
+/* end */
+EOF
+}
+
+# For Python output
+pyOutput() {
+ case "${VCS_WC_MODIFIED}" in
+ 0) VCS_WC_MODIFIED="False" ;;
+ 1) VCS_WC_MODIFIED="True" ;;
+ esac
+ cat > "${TARGETFILE}" << EOF
+# ${GENERATED_HEADER}
+
+VCS_TYPE = "${VCS_TYPE}"
+VCS_BASENAME = "${VCS_BASENAME}"
+VCS_UUID = "${VCS_UUID}"
+VCS_NUM = ${VCS_NUM}
+VCS_DATE = "${VCS_DATE}"
+VCS_BRANCH = "${VCS_BRANCH}"
+VCS_TAG = "${VCS_TAG}"
+VCS_TICK = ${VCS_TICK}
+VCS_EXTRA = "${VCS_EXTRA}"
+
+VCS_ACTION_STAMP = "${VCS_ACTION_STAMP}"
+VCS_FULL_HASH = "${VCS_FULL_HASH}"
+VCS_SHORT_HASH = "${VCS_SHORT_HASH}"
+
+VCS_WC_MODIFIED = ${VCS_WC_MODIFIED}
+
+# end
+EOF
+}
+
+# For Perl output
+plOutput() {
+ cat << EOF
+# ${GENERATED_HEADER}
+
+\$VCS_TYPE = '${VCS_TYPE}';
+\$VCS_BASENAME = '${VCS_BASENAME}';
+\$VCS_UUID = '${VCS_UUID}';
+\$VCS_NUM = ${VCS_NUM};
+\$VCS_DATE = '${VCS_DATE}';
+\$VCS_BRANCH = '${VCS_BRANCH}';
+\$VCS_TAG = '${VCS_TAG}';
+\$VCS_TICK = ${VCS_TICK};
+\$VCS_EXTRA = '${VCS_EXTRA}';
+
+\$VCS_ACTION_STAMP = '${VCS_ACTION_STAMP}';
+\$VCS_FULL_HASH = '${VCS_FULL_HASH}';
+\$VCS_SHORT_HASH = '${VCS_SHORT_HASH}';
+
+\$VCS_WC_MODIFIED = ${VCS_WC_MODIFIED};
+
+# end
+1;
+EOF
+}
+
+# For lua output
+luaOutput() {
+ case "${VCS_WC_MODIFIED}" in
+ 0) VCS_WC_MODIFIED="false" ;;
+ 1) VCS_WC_MODIFIED="true" ;;
+ esac
+ cat > "${TARGETFILE}" << EOF
+-- ${GENERATED_HEADER}
+
+VCS_TYPE = "${VCS_TYPE}"
+VCS_BASENAME = "${VCS_BASENAME}"
+VCS_UUID = "${VCS_UUID}"
+VCS_NUM = ${VCS_NUM}
+VCS_DATE = "${VCS_DATE}"
+VCS_BRANCH = "${VCS_BRANCH}"
+VCS_TAG = "${VCS_TAG}"
+VCS_TICK = ${VCS_TICK}
+VCS_EXTRA = "${VCS_EXTRA}"
+
+VCS_ACTION_STAMP = "${VCS_ACTION_STAMP}"
+VCS_FULL_HASH = "${VCS_FULL_HASH}"
+VCS_SHORT_HASH = "${VCS_SHORT_HASH}"
+
+VCS_WC_MODIFIED = ${VCS_WC_MODIFIED}
+
+-- end
+EOF
+}
+
+# For php output
+phpOutput() {
+ case "${VCS_WC_MODIFIED}" in
+ 0) VCS_WC_MODIFIED="false" ;;
+ 1) VCS_WC_MODIFIED="true" ;;
+ esac
+ cat > "${TARGETFILE}" << EOF
+<?php
+# ${GENERATED_HEADER}
+
+return array(
+ "VCS_TYPE" => "${VCS_TYPE}",
+ "VCS_BASENAME" => "${VCS_BASENAME}",
+ "VCS_UUID" => "${VCS_UUID}",
+ "VCS_NUM" => ${VCS_NUM},
+ "VCS_DATE" => "${VCS_DATE}",
+ "VCS_BRANCH" => "${VCS_BRANCH}",
+ "VCS_TAG" => "${VCS_TAG}",
+ "VCS_TICK" => ${VCS_TICK},
+ "VCS_EXTRA" => "${VCS_EXTRA}",
+ "VCS_ACTION_STAMP" => "${VCS_ACTION_STAMP}",
+ "VCS_FULL_HASH" => "${VCS_FULL_HASH}",
+ "VCS_SHORT_HASH" => "${VCS_SHORT_HASH}",
+ "VCS_WC_MODIFIED" => ${VCS_WC_MODIFIED}
+);
+
+# end
+?>
+EOF
+}
+
+# For ini output
+iniOutput() {
+ case "${VCS_WC_MODIFIED}" in
+ 0) VCS_WC_MODIFIED="false" ;;
+ 1) VCS_WC_MODIFIED="true" ;;
+ esac
+ cat > "${TARGETFILE}" << EOF
+; ${GENERATED_HEADER}
+[VCS]
+VCS_TYPE = "${VCS_TYPE}"
+VCS_BASENAME = "${VCS_BASENAME}"
+VCS_UUID = "${VCS_UUID}"
+VCS_NUM = ${VCS_NUM}
+VCS_DATE = "${VCS_DATE}"
+VCS_BRANCH = "${VCS_BRANCH}"
+VCS_TAG = "${VCS_TAG}"
+VCS_TICK = ${VCS_TICK}
+VCS_EXTRA = "${VCS_EXTRA}"
+VCS_ACTION_STAMP = "${VCS_ACTION_STAMP}"
+VCS_FULL_HASH = "${VCS_FULL_HASH}"
+VCS_SHORT_HASH = "${VCS_SHORT_HASH}"
+VCS_WC_MODIFIED = ${VCS_WC_MODIFIED}
+; end
+EOF
+}
+
+# For javascript output
+jsOutput() {
+ case "${VCS_WC_MODIFIED}" in
+ 1) VCS_WC_MODIFIED="true" ;;
+ 0) VCS_WC_MODIFIED="false" ;;
+ esac
+ cat > "${TARGETFILE}" << EOF
+/** ${GENERATED_HEADER} */
+
+var autorevision = {
+ VCS_TYPE: "${VCS_TYPE}",
+ VCS_BASENAME: "${VCS_BASENAME}",
+ VCS_UUID: "${VCS_UUID}",
+ VCS_NUM: ${VCS_NUM},
+ VCS_DATE: "${VCS_DATE}",
+ VCS_BRANCH: "${VCS_BRANCH}",
+ VCS_TAG: "${VCS_TAG}",
+ VCS_TICK: ${VCS_TICK},
+ VCS_EXTRA: "${VCS_EXTRA}",
+
+ VCS_ACTION_STAMP: "${VCS_ACTION_STAMP}",
+ VCS_FULL_HASH: "${VCS_FULL_HASH}",
+ VCS_SHORT_HASH: "${VCS_SHORT_HASH}",
+
+ VCS_WC_MODIFIED: ${VCS_WC_MODIFIED}
+};
+
+/** Node.js compatibility */
+if (typeof module !== 'undefined') {
+ module.exports = autorevision;
+}
+
+/** end */
+EOF
+}
+
+# For JSON output
+jsonOutput() {
+ case "${VCS_WC_MODIFIED}" in
+ 1) VCS_WC_MODIFIED="true" ;;
+ 0) VCS_WC_MODIFIED="false" ;;
+ esac
+ cat > "${TARGETFILE}" << EOF
+{
+ "_comment": "${GENERATED_HEADER}",
+ "VCS_TYPE": "${VCS_TYPE}",
+ "VCS_BASENAME": "${VCS_BASENAME}",
+ "VCS_UUID": "${VCS_UUID}",
+ "VCS_NUM": ${VCS_NUM},
+ "VCS_DATE": "${VCS_DATE}",
+ "VCS_BRANCH":"${VCS_BRANCH}",
+ "VCS_TAG": "${VCS_TAG}",
+ "VCS_TICK": ${VCS_TICK},
+ "VCS_EXTRA": "${VCS_EXTRA}",
+
+ "VCS_ACTION_STAMP": "${VCS_ACTION_STAMP}",
+ "VCS_FULL_HASH": "${VCS_FULL_HASH}",
+ "VCS_SHORT_HASH": "${VCS_SHORT_HASH}",
+
+ "VCS_WC_MODIFIED": ${VCS_WC_MODIFIED}
+}
+EOF
+}
+
+# For Java output
+javaOutput() {
+ case "${VCS_WC_MODIFIED}" in
+ 1) VCS_WC_MODIFIED="true" ;;
+ 0) VCS_WC_MODIFIED="false" ;;
+ esac
+ cat > "${TARGETFILE}" << EOF
+/* ${GENERATED_HEADER} */
+
+public class autorevision {
+ public static final String VCS_TYPE = "${VCS_TYPE}";
+ public static final String VCS_BASENAME = "${VCS_BASENAME}";
+ public static final String VCS_UUID = "${VCS_UUID}";
+ public static final long VCS_NUM = ${VCS_NUM};
+ public static final String VCS_DATE = "${VCS_DATE}";
+ public static final String VCS_BRANCH = "${VCS_BRANCH}";
+ public static final String VCS_TAG = "${VCS_TAG}";
+ public static final long VCS_TICK = ${VCS_TICK};
+ public static final String VCS_EXTRA = "${VCS_EXTRA}";
+
+ public static final String VCS_ACTION_STAMP = "${VCS_ACTION_STAMP}";
+ public static final String VCS_FULL_HASH = "${VCS_FULL_HASH}";
+ public static final String VCS_SHORT_HASH = "${VCS_SHORT_HASH}";
+
+ public static final boolean VCS_WC_MODIFIED = ${VCS_WC_MODIFIED};
+}
+EOF
+}
+
+# For Java properties output
+javapropOutput() {
+ case "${VCS_WC_MODIFIED}" in
+ 1) VCS_WC_MODIFIED="true" ;;
+ 0) VCS_WC_MODIFIED="false" ;;
+ esac
+ cat > "${TARGETFILE}" << EOF
+# ${GENERATED_HEADER}
+
+VCS_TYPE=${VCS_TYPE}
+VCS_BASENAME=${VCS_BASENAME}
+VCS_UUID=${VCS_UUID}
+VCS_NUM=${VCS_NUM}
+VCS_DATE=${VCS_DATE}
+VCS_BRANCH=${VCS_BRANCH}
+VCS_TAG=${VCS_TAG}
+VCS_TICK=${VCS_TICK}
+VCS_EXTRA=${VCS_EXTRA}
+
+VCS_ACTION_STAMP=${VCS_ACTION_STAMP}
+VCS_FULL_HASH=${VCS_FULL_HASH}
+VCS_SHORT_HASH=${VCS_SHORT_HASH}
+
+VCS_WC_MODIFIED=${VCS_WC_MODIFIED}
+EOF
+}
+
+# For m4 output
+m4Output() {
+ cat > "${TARGETFILE}" << EOF
+dnl ${GENERATED_HEADER}
+define(\`VCS_TYPE', \`${VCS_TYPE}')dnl
+define(\`VCS_BASENAME', \`${VCS_BASENAME}')dnl
+define(\`VCS_UUID', \`${VCS_UUID}')dnl
+define(\`VCS_NUM', \`${VCS_NUM}')dnl
+define(\`VCS_DATE', \`${VCS_DATE}')dnl
+define(\`VCS_BRANCH', \`${VCS_BRANCH}')dnl
+define(\`VCS_TAG', \`${VCS_TAG}')dnl
+define(\`VCS_TICK', \`${VCS_TICK}')dnl
+define(\`VCS_EXTRA', \`${VCS_EXTRA}')dnl
+define(\`VCS_ACTIONSTAMP', \`${VCS_ACTION_STAMP}')dnl
+define(\`VCS_FULLHASH', \`${VCS_FULL_HASH}')dnl
+define(\`VCS_SHORTHASH', \`${VCS_SHORT_HASH}')dnl
+define(\`VCS_WC_MODIFIED', \`${VCS_WC_MODIFIED}')dnl
+EOF
+}
+
+# For (La)TeX output
+texOutput() {
+ case "${VCS_WC_MODIFIED}" in
+ 0) VCS_WC_MODIFIED="false" ;;
+ 1) VCS_WC_MODIFIED="true" ;;
+ esac
+ cat > "${TARGETFILE}" << EOF
+% ${GENERATED_HEADER}
+\def \vcsType {${VCS_TYPE}}
+\def \vcsBasename {${VCS_BASENAME}}
+\def \vcsUUID {${VCS_UUID}}
+\def \vcsNum {${VCS_NUM}}
+\def \vcsDate {${VCS_DATE}}
+\def \vcsBranch {${VCS_BRANCH}}
+\def \vcsTag {${VCS_TAG}}
+\def \vcsTick {${VCS_TICK}}
+\def \vcsExtra {${VCS_EXTRA}}
+\def \vcsACTIONSTAMP {${VCS_ACTION_STAMP}}
+\def \vcsFullHash {${VCS_FULL_HASH}}
+\def \vcsShortHash {${VCS_SHORT_HASH}}
+\def \vcsWCModified {${VCS_WC_MODIFIED}}
+\endinput
+EOF
+}
+
+# For scheme output
+schemeOutput() {
+ case "${VCS_WC_MODIFIED}" in
+ 0) VCS_WC_MODIFIED="#f" ;;
+ 1) VCS_WC_MODIFIED="#t" ;;
+ esac
+ cat > "${TARGETFILE}" << EOF
+;; ${GENERATED_HEADER}
+(define VCS_TYPE "${VCS_TYPE}")
+(define VCS_BASENAME "${VCS_BASENAME}")
+(define VCS_UUID "${VCS_UUID}")
+(define VCS_NUM ${VCS_NUM})
+(define VCS_DATE "${VCS_DATE}")
+(define VCS_BRANCH "${VCS_BRANCH}")
+(define VCS_TAG "${VCS_TAG}")
+(define VCS_TICK ${VCS_TICK})
+(define VCS_EXTRA "${VCS_EXTRA}")
+
+(define VCS_ACTION_STAMP "${VCS_ACTION_STAMP}")
+(define VCS_FULL_HASH "${VCS_FULL_HASH}")
+(define VCS_SHORT_HASH "${VCS_SHORT_HASH}")
+
+(define VCS_WC_MODIFIED ${VCS_WC_MODIFIED})
+;; end
+EOF
+}
+
+# For clojure output
+clojureOutput() {
+ case "${VCS_WC_MODIFIED}" in
+ 0) VCS_WC_MODIFIED="false" ;;
+ 1) VCS_WC_MODIFIED="true" ;;
+ esac
+ cat > "${TARGETFILE}" << EOF
+;; ${GENERATED_HEADER}
+(def VCS_TYPE "${VCS_TYPE}")
+(def VCS_BASENAME "${VCS_BASENAME}")
+(def VCS_UUID "${VCS_UUID}")
+(def VCS_NUM ${VCS_NUM})
+(def VCS_DATE "${VCS_DATE}")
+(def VCS_BRANCH "${VCS_BRANCH}")
+(def VCS_TAG "${VCS_TAG}")
+(def VCS_TICK ${VCS_TICK})
+(def VCS_EXTRA "${VCS_EXTRA}")
+
+(def VCS_ACTION_STAMP "${VCS_ACTION_STAMP}")
+(def VCS_FULL_HASH "${VCS_FULL_HASH}")
+(def VCS_SHORT_HASH "${VCS_SHORT_HASH}")
+
+(def VCS_WC_MODIFIED ${VCS_WC_MODIFIED})
+;; end
+EOF
+}
+
+# For rpm spec file output
+rpmOutput() {
+ cat > "${TARGETFILE}" << EOF
+# ${GENERATED_HEADER}
+$([ "${VCS_TYPE}" ] && echo "%define vcs_type ${VCS_TYPE}")
+$([ "${VCS_BASENAME}" ] && echo "%define vcs_basename ${VCS_BASENAME}")
+$([ "${VCS_UUID}" ] && echo "%define vcs_uuid ${VCS_UUID}")
+$([ "${VCS_NUM}" ] && echo "%define vcs_num ${VCS_NUM}")
+$([ "${VCS_DATE}" ] && echo "%define vcs_date ${VCS_DATE}")
+$([ "${VCS_BRANCH}" ] && echo "%define vcs_branch ${VCS_BRANCH}")
+$([ "${VCS_TAG}" ] && echo "%define vcs_tag ${VCS_TAG}")
+$([ "${VCS_TICK}" ] && echo "%define vcs_tick ${VCS_TICK}")
+$([ "${VCS_EXTRA}" ] && echo "%define vcs_extra ${VCS_EXTRA}")
+
+$([ "${VCS_ACTION_STAMP}" ] && echo "%define vcs_action_stamp ${VCS_ACTION_STAMP}")
+$([ "${VCS_FULL_HASH}" ] && echo "%define vcs_full_hash ${VCS_FULL_HASH}")
+$([ "${VCS_SHORT_HASH}" ] && echo "%define vcs_short_hash ${VCS_SHORT_HASH}")
+
+$([ "${VCS_WC_MODIFIED}" ] && echo "%define vcs_wc_modified ${VCS_WC_MODIFIED}")
+# end
+EOF
+}
+
+# shellcheck disable=SC2155,SC2039
+hppOutput() {
+ local NAMESPACE="$(echo "${VCS_BASENAME}" | sed -e 's:_::g' | tr '[:lower:]' '[:upper:]')"
+ cat > "${TARGETFILE}" << EOF
+/* ${GENERATED_HEADER} */
+
+#ifndef ${NAMESPACE}_AUTOREVISION_H
+#define ${NAMESPACE}_AUTOREVISION_H
+
+#include <string>
+
+namespace $(echo "${NAMESPACE}" | tr '[:upper:]' '[:lower:]')
+{
+ const std::string VCS_TYPE = "${VCS_TYPE}";
+ const std::string VCS_BASENAME = "${VCS_BASENAME}";
+ const std::string VCS_UUID = "${VCS_UUID}";
+ const int VCS_NUM = ${VCS_NUM};
+ const std::string VCS_DATE = "${VCS_DATE}";
+ const std::string VCS_BRANCH = "${VCS_BRANCH}";
+ const std::string VCS_TAG = "${VCS_TAG}";
+ const int VCS_TICK = ${VCS_TICK};
+ const std::string VCS_EXTRA = "${VCS_EXTRA}";
+
+ const std::string VCS_ACTION_STAMP = "${VCS_ACTION_STAMP}";
+ const std::string VCS_FULL_HASH = "${VCS_FULL_HASH}";
+ const std::string VCS_SHORT_HASH = "${VCS_SHORT_HASH}";
+
+ const int VCS_WC_MODIFIED = ${VCS_WC_MODIFIED};
+}
+
+#endif
+
+/* end */
+EOF
+}
+
+matlabOutput() {
+ case "${VCS_WC_MODIFIED}" in
+ 0) VCS_WC_MODIFIED="FALSE" ;;
+ 1) VCS_WC_MODIFIED="TRUE" ;;
+ esac
+ cat > "${TARGETFILE}" << EOF
+% ${GENERATED_HEADER}
+
+VCS_TYPE = '${VCS_TYPE}';
+VCS_BASENAME = '${VCS_BASENAME}';
+VCS_UUID = '${VCS_UUID}';
+VCS_NUM = ${VCS_NUM};
+VCS_DATE = '${VCS_DATE}';
+VCS_BRANCH = '${VCS_BRANCH}';
+VCS_TAG = '${VCS_TAG}';
+VCS_TICK = ${VCS_TICK};
+VCS_EXTRA = '${VCS_EXTRA}';
+
+VCS_ACTION_STAMP = '${VCS_ACTION_STAMP}';
+VCS_FULL_HASH = '${VCS_FULL_HASH}';
+VCS_SHORT_HASH = '${VCS_SHORT_HASH}';
+
+VCS_WC_MODIFIED = ${VCS_WC_MODIFIED};
+
+% end
+EOF
+}
+
+octaveOutput() {
+ cat > "${TARGETFILE}" << EOF
+% ${GENERATED_HEADER}
+
+VCS_TYPE = '${VCS_TYPE}';
+VCS_BASENAME = '${VCS_BASENAME}';
+VCS_UUID = '${VCS_UUID}';
+VCS_NUM = ${VCS_NUM};
+VCS_DATE = '${VCS_DATE}';
+VCS_BRANCH = '${VCS_BRANCH}';
+VCS_TAG = '${VCS_TAG}';
+VCS_TICK = ${VCS_TICK};
+VCS_EXTRA = '${VCS_EXTRA}';
+
+VCS_ACTION_STAMP = '${VCS_ACTION_STAMP}';
+VCS_FULL_HASH = '${VCS_FULL_HASH}';
+VCS_SHORT_HASH = '${VCS_SHORT_HASH}';
+
+VCS_WC_MODIFIED = ${VCS_WC_MODIFIED};
+
+% end
+EOF
+}
+
+cmakeOutput() {
+ cat > "${TARGETFILE}" << EOF
+# ${GENERATED_HEADER}
+
+set(VCS_TYPE ${VCS_TYPE})
+set(VCS_BASENAME ${VCS_BASENAME})
+set(VCS_UUID ${VCS_UUID})
+set(VCS_NUM ${VCS_NUM})
+set(VCS_DATE ${VCS_DATE})
+set(VCS_BRANCH ${VCS_BRANCH})
+set(VCS_TAG ${VCS_TAG})
+set(VCS_TICK ${VCS_TICK})
+set(VCS_EXTRA ${VCS_EXTRA})
+
+set(VCS_ACTION_STAMP ${VCS_ACTION_STAMP})
+set(VCS_FULL_HASH ${VCS_FULL_HASH})
+set(VCS_SHORT_HASH ${VCS_SHORT_HASH})
+
+set(VCS_WC_MODIFIED ${VCS_WC_MODIFIED})
+
+# end
+EOF
+}
+
+
+# Helper functions
+# Count path segments
+# shellcheck disable=SC2039
+pathSegment() {
+ local pathz="${1}"
+ local depth="0"
+
+ if [ ! -z "${pathz}" ]; then
+ # Continue until we are at / or there are no path separators left.
+ while [ ! "${pathz}" = "/" ] && [ ! "${pathz}" = "$(echo "${pathz}" | sed -e 's:/::')" ]; do
+ pathz="$(dirname "${pathz}")"
+ depth="$((depth+1))"
+ done
+ fi
+ echo "${depth}"
+}
+
+# Largest of four numbers
+# shellcheck disable=SC2039
+multiCompare() {
+ local larger="${1}"
+ local numA="${2}"
+ local numB="${3}"
+ local numC="${4}"
+
+ [ "${numA}" -gt "${larger}" ] && larger="${numA}"
+ [ "${numB}" -gt "${larger}" ] && larger="${numB}"
+ [ "${numC}" -gt "${larger}" ] && larger="${numC}"
+ echo "${larger}"
+}
+
+# Test for repositories
+# shellcheck disable=SC2155,SC2039
+repoTest() {
+ REPONUM="0"
+ if [ ! -z "$(git rev-parse HEAD 2>/dev/null)" ]; then
+ local gitPath="$(git rev-parse --show-toplevel)"
+ local gitDepth="$(pathSegment "${gitPath}")"
+ REPONUM="$((REPONUM+1))"
+ else
+ local gitDepth="0"
+ fi
+ if [ ! -z "$(hg root 2>/dev/null)" ]; then
+ local hgPath="$(hg root 2>/dev/null)"
+ local hgDepth="$(pathSegment "${hgPath}")"
+ REPONUM="$((REPONUM+1))"
+ else
+ local hgDepth="0"
+ fi
+ if [ ! -z "$(bzr root 2>/dev/null)" ]; then
+ local bzrPath="$(bzr root 2>/dev/null)"
+ local bzrDepth="$(pathSegment "${bzrPath}")"
+ REPONUM="$((REPONUM+1))"
+ else
+ local bzrDepth="0"
+ fi
+ if [ ! -z "$(svn info 2>/dev/null)" ]; then
+ local stringz="<wcroot-abspath>"
+ local stringx="</wcroot-abspath>"
+ local svnPath="$(svn info --xml | sed -n -e "s:${stringz}::" -e "s:${stringx}::p")"
+ # An old enough svn will not be able give us a path; default
+ # to 1 for that case.
+ if [ -z "${svnPath}" ]; then
+ local svnDepth="1"
+ else
+ local svnDepth="$(pathSegment "${svnPath}")"
+ fi
+ REPONUM="$((REPONUM+1))"
+ else
+ local svnDepth="0"
+ fi
+
+ # Do not do more work then we have to.
+ if [ "${REPONUM}" = "0" ]; then
+ return
+ fi
+
+ # Figure out which repo is the deepest and use it.
+ local wonRepo="$(multiCompare "${gitDepth}" "${hgDepth}" "${bzrDepth}" "${svnDepth}")"
+ if [ "${wonRepo}" = "${gitDepth}" ]; then
+ gitRepo
+ elif [ "${wonRepo}" = "${hgDepth}" ]; then
+ hgRepo
+ elif [ "${wonRepo}" = "${bzrDepth}" ]; then
+ bzrRepo
+ elif [ "${wonRepo}" = "${svnDepth}" ]; then
+ svnRepo
+ fi
+}
+
+
+
+# Detect which repos we are in and gather data.
+# shellcheck source=/dev/null
+if [ -f "${CACHEFILE}" ] && [ "${CACHEFORCE}" = "1" ]; then
+ # When requested only read from the cache to populate our symbols.
+ . "${CACHEFILE}"
+else
+ # If a value is not set through the environment set VCS_EXTRA to nothing.
+ : "${VCS_EXTRA:=""}"
+ repoTest
+
+ if [ -f "${CACHEFILE}" ] && [ "${REPONUM}" = "0" ]; then
+ # We are not in a repo; try to use a previously generated cache to populate our symbols.
+ . "${CACHEFILE}"
+ # Do not overwrite the cache if we know we are not going to write anything new.
+ CACHEFORCE="1"
+ elif [ "${REPONUM}" = "0" ]; then
+ echo "error: No repo or cache detected." 1>&2
+ exit 1
+ fi
+fi
+
+
+# -s output is handled here.
+if [ ! -z "${VAROUT}" ]; then
+ if [ "${VAROUT}" = "VCS_TYPE" ]; then
+ echo "${VCS_TYPE}"
+ elif [ "${VAROUT}" = "VCS_BASENAME" ]; then
+ echo "${VCS_BASENAME}"
+ elif [ "${VAROUT}" = "VCS_NUM" ]; then
+ echo "${VCS_NUM}"
+ elif [ "${VAROUT}" = "VCS_DATE" ]; then
+ echo "${VCS_DATE}"
+ elif [ "${VAROUT}" = "VCS_BRANCH" ]; then
+ echo "${VCS_BRANCH}"
+ elif [ "${VAROUT}" = "VCS_TAG" ]; then
+ echo "${VCS_TAG}"
+ elif [ "${VAROUT}" = "VCS_TICK" ]; then
+ echo "${VCS_TICK}"
+ elif [ "${VAROUT}" = "VCS_FULL_HASH" ]; then
+ echo "${VCS_FULL_HASH}"
+ elif [ "${VAROUT}" = "VCS_SHORT_HASH" ]; then
+ echo "${VCS_SHORT_HASH}"
+ elif [ "${VAROUT}" = "VCS_WC_MODIFIED" ]; then
+ echo "${VCS_WC_MODIFIED}"
+ elif [ "${VAROUT}" = "VCS_ACTION_STAMP" ]; then
+ echo "${VCS_ACTION_STAMP}"
+ else
+ echo "error: Not a valid output symbol." 1>&2
+ exit 1
+ fi
+fi
+
+
+# Detect requested output type and use it.
+if [ ! -z "${AFILETYPE}" ]; then
+ if [ "${AFILETYPE}" = "c" ]; then
+ cOutput
+ elif [ "${AFILETYPE}" = "h" ]; then
+ hOutput
+ elif [ "${AFILETYPE}" = "xcode" ]; then
+ xcodeOutput
+ elif [ "${AFILETYPE}" = "swift" ]; then
+ swiftOutput
+ elif [ "${AFILETYPE}" = "sh" ]; then
+ shOutput
+ elif [ "${AFILETYPE}" = "py" ] || [ "${AFILETYPE}" = "python" ]; then
+ pyOutput
+ elif [ "${AFILETYPE}" = "pl" ] || [ "${AFILETYPE}" = "perl" ]; then
+ plOutput
+ elif [ "${AFILETYPE}" = "lua" ]; then
+ luaOutput
+ elif [ "${AFILETYPE}" = "php" ]; then
+ phpOutput
+ elif [ "${AFILETYPE}" = "ini" ]; then
+ iniOutput
+ elif [ "${AFILETYPE}" = "js" ]; then
+ jsOutput
+ elif [ "${AFILETYPE}" = "json" ]; then
+ jsonOutput
+ elif [ "${AFILETYPE}" = "java" ]; then
+ javaOutput
+ elif [ "${AFILETYPE}" = "javaprop" ]; then
+ javapropOutput
+ elif [ "${AFILETYPE}" = "tex" ]; then
+ texOutput
+ elif [ "${AFILETYPE}" = "m4" ]; then
+ m4Output
+ elif [ "${AFILETYPE}" = "scheme" ]; then
+ schemeOutput
+ elif [ "${AFILETYPE}" = "clojure" ]; then
+ clojureOutput
+ elif [ "${AFILETYPE}" = "rpm" ]; then
+ rpmOutput
+ elif [ "${AFILETYPE}" = "hpp" ]; then
+ hppOutput
+ elif [ "${AFILETYPE}" = "matlab" ]; then
+ matlabOutput
+ elif [ "${AFILETYPE}" = "octave" ]; then
+ octaveOutput
+ elif [ "${AFILETYPE}" = "cmake" ]; then
+ cmakeOutput
+ else
+ echo "error: Not a valid output type." 1>&2
+ exit 1
+ fi
+fi
+
+
+# If requested, make a cache file.
+if [ ! -z "${CACHEFILE}" ] && [ ! "${CACHEFORCE}" = "1" ]; then
+ TARGETFILE="${CACHEFILE}.tmp"
+ shOutput
+
+ # Check to see if there have been any actual changes.
+ if [ ! -f "${CACHEFILE}" ]; then
+ mv -f "${CACHEFILE}.tmp" "${CACHEFILE}"
+ elif cmp -s "${CACHEFILE}.tmp" "${CACHEFILE}"; then
+ rm -f "${CACHEFILE}.tmp"
+ else
+ mv -f "${CACHEFILE}.tmp" "${CACHEFILE}"
+ fi
+fi
diff --git a/bin/ftp.conf b/bin/ftp.conf
new file mode 100644
index 0000000..7d8c6bf
--- /dev/null
+++ b/bin/ftp.conf
@@ -0,0 +1,3 @@
+[FTP]
+log_level=10
+log_path=./log/ftp_decoder.log
diff --git a/bin/ftp_decoder.inf b/bin/ftp_decoder.inf
new file mode 100644
index 0000000..3e3005d
--- /dev/null
+++ b/bin/ftp_decoder.inf
@@ -0,0 +1,9 @@
+[[plugin]]
+path = "./stellar_plugin/ftp_decoder/ftp_decoder.so"
+init = "FTP_ONLOAD"
+exit = "FTP_UNLOAD"
+
+[[plugin]]
+path = "./stellar_plugin/ftp_decoder/ftp_decoder_test_plug.so"
+init = "FTP_TEST_PLUG_INIT"
+exit = "FTP_TEST_PLUG_DESTROY"
diff --git a/ci/get-nprocessors.sh b/ci/get-nprocessors.sh
new file mode 100644
index 0000000..43635e7
--- /dev/null
+++ b/ci/get-nprocessors.sh
@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+# Copyright 2017 Google Inc.
+# All Rights Reserved.
+#
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# This file is typically sourced by another script.
+# if possible, ask for the precise number of processors,
+# otherwise take 2 processors as reasonable default; see
+# https://docs.travis-ci.com/user/speeding-up-the-build/#Makefile-optimization
+if [ -x /usr/bin/getconf ]; then
+ NPROCESSORS=$(/usr/bin/getconf _NPROCESSORS_ONLN)
+else
+ NPROCESSORS=2
+fi
+
+# as of 2017-09-04 Travis CI reports 32 processors, but GCC build
+# crashes if parallelized too much (maybe memory consumption problem),
+# so limit to 4 processors for the time being.
+if [ $NPROCESSORS -gt 4 ] ; then
+ echo "$0:Note: Limiting processors to use by make from $NPROCESSORS to 4."
+ NPROCESSORS=4
+fi
diff --git a/ci/perpare_pulp3_netrc.sh b/ci/perpare_pulp3_netrc.sh
new file mode 100644
index 0000000..8414bbb
--- /dev/null
+++ b/ci/perpare_pulp3_netrc.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env sh
+set -evx
+echo "machine ${PULP3_SERVER_URL}\nlogin ${PULP3_SERVER_LOGIN}\npassword ${PULP3_SERVER_PASSWORD}\n" > ~/.netrc
diff --git a/ci/travis.sh b/ci/travis.sh
new file mode 100644
index 0000000..7a4cdc1
--- /dev/null
+++ b/ci/travis.sh
@@ -0,0 +1,69 @@
+#!/usr/bin/env sh
+set -evx
+
+chmod +x ci/get-nprocessors.sh
+. ci/get-nprocessors.sh
+
+# if possible, ask for the precise number of processors,
+# otherwise take 2 processors as reasonable default; see
+# https://docs.travis-ci.com/user/speeding-up-the-build/#Makefile-optimization
+if [ -x /usr/bin/getconf ]; then
+ NPROCESSORS=$(/usr/bin/getconf _NPROCESSORS_ONLN)
+else
+ NPROCESSORS=2
+fi
+
+# as of 2017-09-04 Travis CI reports 32 processors, but GCC build
+# crashes if parallelized too much (maybe memory consumption problem),
+# so limit to 4 processors for the time being.
+if [ $NPROCESSORS -gt 4 ] ; then
+ echo "$0:Note: Limiting processors to use by make from $NPROCESSORS to 4."
+ NPROCESSORS=4
+fi
+
+# Tell make to use the processors. No preceding '-' required.
+MAKEFLAGS="j${NPROCESSORS}"
+export MAKEFLAGS
+
+env | sort
+
+# Set default values to OFF for these variables if not specified.
+: "${NO_EXCEPTION:=OFF}"
+: "${NO_RTTI:=OFF}"
+: "${COMPILER_IS_GNUCXX:=OFF}"
+
+# Install dependency from YUM
+if [ -n "${INSTALL_DEPENDENCY_FRAMEWORK}" ]; then
+ yum install -y $INSTALL_DEPENDENCY_FRAMEWORK
+ source /etc/profile.d/framework.sh
+fi
+
+if [ -n "${INSTALL_DEPENDENCY_PLATFORM}" ]; then
+ yum install -y $INSTALL_DEPENDENCY_PLATFORM
+fi
+
+if [ $ASAN_OPTION ] && [ -f "/opt/rh/devtoolset-7/enable" ] ;then
+ source /opt/rh/devtoolset-7/enable
+fi
+
+mkdir build || true
+cd build
+
+cmake3 -DCMAKE_CXX_FLAGS=$CXX_FLAGS \
+ -DCMAKE_BUILD_TYPE=$BUILD_TYPE \
+ -DCMAKE_INSTALL_PREFIX=$INSTALL_PREFIX \
+ -DASAN_OPTION=$ASAN_OPTION \
+ -DVERSION_DAILY_BUILD=$TESTING_VERSION_BUILD \
+ ..
+
+make
+
+if [ -n "${PACKAGE}" ]; then
+ make package
+fi
+
+if [ -n "${UPLOAD_RPM}" ]; then
+ cp ~/rpm_upload_tools.py ./
+ python3 rpm_upload_tools.py ${PULP3_REPO_NAME} ${PULP3_DIST_NAME} *.rpm
+fi
+
diff --git a/cmake/Package.cmake b/cmake/Package.cmake
new file mode 100644
index 0000000..d7c7dd3
--- /dev/null
+++ b/cmake/Package.cmake
@@ -0,0 +1,56 @@
+if(CMAKE_BUILD_TYPE STREQUAL "Debug")
+ set(MY_RPM_NAME_PREFIX "${lib_name}-debug")
+else()
+ set(MY_RPM_NAME_PREFIX "${lib_name}")
+endif()
+
+message(STATUS "Package: ${MY_RPM_NAME_PREFIX}")
+
+set(CPACK_PACKAGE_VECDOR "MESA")
+set(CPACK_PACKAGE_VERSION_MAJOR "${VERSION_MAJOR}")
+set(CPACK_PACKAGE_VERSION_MINOR "${VERSION_MINOR}")
+set(CPACK_PACKAGE_VERSION_PATCH "${VERSION_PATCH}.${VERSION_BUILD}")
+set(CPACK_PACKAGING_INSTALL_PREFIX ${CMAKE_INSTALL_PREFIX})
+set(CPACK_PACKAGE_VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}.${VERSION_BUILD}")
+
+execute_process(COMMAND sh changelog.sh ${CMAKE_BINARY_DIR} WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/cmake)
+SET(CPACK_RPM_CHANGELOG_FILE ${CMAKE_BINARY_DIR}/changelog.txt)
+
+# RPM Build
+set(CPACK_GENERATOR "RPM")
+set(CPACK_RPM_AUTO_GENERATED_FILE_NAME ON)
+set(CPACK_RPM_FILE_NAME "RPM-DEFAULT")
+set(CPACK_RPM_PACKAGE_VENDOR "MESA")
+set(CPACK_RPM_PACKAGE_AUTOREQPROV "yes")
+set(CPACK_RPM_PACKAGE_RELEASE_DIST "on")
+set(CPACK_RPM_DEBUGINFO_PACKAGE "on")
+
+set(CPACK_RPM_COMPONENT_INSTALL ON)
+set(CPACK_COMPONENTS_IGNORE_GROUPS 1)
+set(CPACK_COMPONENTS_GROUPING ONE_PER_GROUP)
+set(CPACK_COMPONENT_HEADER_DISPLAY_NAME "develop")
+
+set(CPACK_COMPONENT_LIBRARIES_REQUIRED TRUE)
+set(CPACK_RPM_LIBRARIES_PACKAGE_NAME ${MY_RPM_NAME_PREFIX})
+set(CPACK_COMPONENT_LIBRARIES_GROUP "LIBRARIES")
+set(CPACK_COMPONENT_PROFILE_GROUP "LIBRARIES")
+
+set(CPACK_COMPONENT_HEADER_REQUIRED TRUE)
+set(CPACK_RPM_HEADER_PACKAGE_NAME "${MY_RPM_NAME_PREFIX}-devel")
+set(CPACK_COMPONENT_HEADER_GROUP "HEADER")
+
+set(CPACK_RPM_HEADER_PACKAGE_REQUIRES_PRE ${CPACK_RPM_LIBRARIES_PACKAGE_NAME})
+set(CPACK_RPM_HEADER_PACKAGE_CONFLICTS ${CPACK_RPM_HEADER_PACKAGE_NAME})
+
+set(CPACK_COMPONENTS_ALL LIBRARIES HEADER PROFILE)
+
+SET(CPACK_RPM_LIBRARIES_PRE_INSTALL_SCRIPT_FILE "${PROJECT_SOURCE_DIR}/cmake/preInstall.sh")
+SET(CPACK_RPM_LIBRARIES_PRE_UNINSTALL_SCRIPT_FILE "${PROJECT_SOURCE_DIR}/cmake/preUninstall.sh")
+
+set(CPACK_BUILD_SOURCE_DIRS "${CMAKE_SOURCE_DIR}")
+
+# Must uninstall the debug package before install release package
+set(CPACK_RPM_PACKAGE_CONFLICTS ${MY_RPM_NAME_PREFIX})
+
+# set(CPACK_STRIP_FILES TRUE)
+include(CPack)
diff --git a/cmake/Version.cmake b/cmake/Version.cmake
new file mode 100644
index 0000000..a47944c
--- /dev/null
+++ b/cmake/Version.cmake
@@ -0,0 +1,54 @@
+
+# Using autorevision.sh to generate version information
+
+set(__SOURCE_AUTORESIVISION ${CMAKE_SOURCE_DIR}/autorevision.sh)
+set(__AUTORESIVISION ${CMAKE_BINARY_DIR}/autorevision.sh)
+set(__VERSION_CACHE ${CMAKE_BINARY_DIR}/version.txt)
+set(__VERSION_CONFIG ${CMAKE_BINARY_DIR}/version.cmake)
+
+file(COPY ${__SOURCE_AUTORESIVISION} DESTINATION ${CMAKE_BINARY_DIR}
+ FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE
+ WORLD_READ WORLD_EXECUTE)
+
+# execute autorevision.sh to generate version information
+execute_process(COMMAND ${__AUTORESIVISION} -t cmake -o ${__VERSION_CACHE}
+ OUTPUT_FILE ${__VERSION_CONFIG} ERROR_QUIET)
+include(${__VERSION_CONFIG})
+
+# extract major, minor, patch version from git tag
+string(REGEX REPLACE "^v([0-9]+)\\..*" "\\1" VERSION_MAJOR "${VCS_TAG}")
+string(REGEX REPLACE "^v[0-9]+\\.([0-9]+).*" "\\1" VERSION_MINOR "${VCS_TAG}")
+string(REGEX REPLACE "^v[0-9]+\\.[0-9]+\\.([0-9]+).*" "\\1" VERSION_PATCH "${VCS_TAG}")
+string(REGEX REPLACE "[T\\:\\+\\-]" "" VERSION_DATE "${VCS_DATE}")
+
+if(VERSION_DAILY_BUILD)
+ set(VERSION_PATCH ${VERSION_PATCH}.${VERSION_DATE})
+endif()
+
+if(NOT VERSION_MAJOR)
+ set(VERSION_MAJOR 1)
+endif()
+
+if(NOT VERSION_MINOR)
+ set(VERSION_MINOR 0)
+endif()
+
+if(NOT VERSION_PATCH)
+ set(VERSION_PATCH 0)
+endif()
+
+set(VERSION "${VERSION_MAJOR}_${VERSION_MINOR}_${VERSION_PATCH}")
+set(VERSION_BUILD "${VCS_SHORT_HASH}")
+
+# print information
+message(STATUS "Version: ${VERSION}-${VERSION_BUILD}")
+
+option(DEFINE_GIT_VERSION "Set DEFINE_GIT_VERSION to TRUE or FALSE" TRUE)
+
+if(DEFINE_GIT_VERSION)
+ set(GIT_VERSION
+ "${VERSION}-${CMAKE_BUILD_TYPE}-${VERSION_BUILD}-${VCS_BRANCH}-${VCS_TAG}-${VCS_DATE}")
+ string(REGEX REPLACE "[-:+/\\.]" "_" GIT_VERSION ${GIT_VERSION})
+
+ add_definitions(-DGIT_VERSION=${GIT_VERSION})
+endif()
diff --git a/cmake/changelog.sh b/cmake/changelog.sh
new file mode 100644
index 0000000..087129d
--- /dev/null
+++ b/cmake/changelog.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+work_path=$1
+branch=`git status | grep 'On branch' | awk '{print $NF}'`
+git log --branches=$branch --no-merges --date=local --show-signature --pretty="* %ad %an %ae %nhash: %H%ncommit:%n%B" | awk -F"-" '{print "- "$0}' | sed 's/- \*/\*/g' | sed 's/- $//g' | sed 's/-/ -/g' | sed 's/[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}//g' > $work_path/changelog.txt
diff --git a/cmake/preInstall.sh b/cmake/preInstall.sh
new file mode 100644
index 0000000..0668303
--- /dev/null
+++ b/cmake/preInstall.sh
@@ -0,0 +1,14 @@
+#!/bin/sh
+DST=${RPM_INSTALL_PREFIX}/sapp/
+
+mkdir -p ${DST}/stellar_plugin
+touch ${DST}/stellar_plugin/spec.toml
+
+if ! grep -q '^\./plug/stellar_plugin/ftp_decoder/ftp_decoder.so' "${DST}/stellar_plugin/spec.toml"; then
+ echo -e "\n" >> "${DST}/stellar_plugin/spec.toml"
+ echo -e "[[plugin]]" >> "${DST}/stellar_plugin/spec.toml"
+ echo -e "path = \"./stellar_plugin/ftp_decoder/ftp_decoder.so\"" >> "${DST}/stellar_plugin/spec.toml"
+ echo -e "init = \"FTP_ONLOAD\"" >> "${DST}/stellar_plugin/spec.toml"
+ echo -e "exit = \"FTP_UNLOAD\"" >> "${DST}/stellar_plugin/spec.toml"
+ echo -e "\n" >> "${DST}/stellar_plugin/spec.toml"
+fi
diff --git a/cmake/preUninstall.sh b/cmake/preUninstall.sh
new file mode 100644
index 0000000..3e54faf
--- /dev/null
+++ b/cmake/preUninstall.sh
@@ -0,0 +1,6 @@
+if [ $1 == 0 ]; then
+ DST=${RPM_INSTALL_PREFIX}/sapp/stellar_plugin
+ sed -i -n '$!N;/ftp_decoder.so/!P;D' ${DST}/spec.toml
+ sed -i '/FTP_ONLOAD/d' ${DST}/spec.toml
+ sed -i '/FTP_UNLOAD/d' ${DST}/spec.toml
+fi
diff --git a/include/ftp_decoder.h b/include/ftp_decoder.h
new file mode 100644
index 0000000..206ece9
--- /dev/null
+++ b/include/ftp_decoder.h
@@ -0,0 +1,38 @@
+#pragma once
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+#include <bits/types/struct_iovec.h>
+
+#define FTP_DECODER_CTRL_TOPIC "FTP_DECODER_CTRL_MESSAGE"
+#define FTP_DECODER_DATA_TOPIC "FTP_DECODER_DATA_MESSAGE"
+
+struct ftp_message;
+
+enum ftp_msg_type{
+ FTP_BANNER = 0,
+ FTP_ACCOUNT,
+ FTP_PASSWORD,
+ FTP_URI,
+ FTP_TRANS_MODE, // "PASV", "PORT"
+ FTP_TRANS_DIR, // "RETR", "STOR"
+ FTP_INVENTORY, // by command "LIST"
+ FTP_FILE_CONTENT, // by command "RETR" and "STOR"
+
+ FTP_MSG_MAX,
+};
+
+/**
+ * 1: success
+ * 0: failed or no available data
+*/
+int ftp_message_iterate(const struct ftp_message *msg, enum ftp_msg_type *type, struct iovec *value);
+
+void ftp_message_reset_iterator(const struct ftp_message *msg);
+
+const char *ftp_message_type_to_string(enum ftp_msg_type fmsg_type);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 0000000..075e7fd
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,15 @@
+cmake_minimum_required (VERSION 2.8...3.10)
+
+file(GLOB SRC "*.cpp")
+set(DEPEND_DYN_LIB MESA_handle_logger MESA_prof_load MESA_htable fieldstat4)
+
+include_directories(/opt/tsg/framework/include/)
+
+# Shared Library Output
+add_library(${lib_name} SHARED ${SRC})
+set_target_properties(${lib_name} PROPERTIES LINK_FLAGS "-Wl,--version-script=${PROJECT_SOURCE_DIR}/src/version.map")
+set_target_properties(${lib_name} PROPERTIES PREFIX "")
+target_link_libraries(${lib_name} ${DEPEND_DYN_LIB} )
+set_target_properties(${lib_name} PROPERTIES OUTPUT_NAME ${lib_name})
+
+install(TARGETS ${lib_name} LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}/sapp/stellar_plugin/ftp_decoder COMPONENT LIBRARIES)
diff --git a/src/ftp_decoder_entry.cpp b/src/ftp_decoder_entry.cpp
new file mode 100644
index 0000000..3dc0df0
--- /dev/null
+++ b/src/ftp_decoder_entry.cpp
@@ -0,0 +1,227 @@
+#include "ftp_decoder_inner.h"
+#include "ftp_decoder.h"
+#include "ftp_decoder_util.h"
+#include <stellar/session.h>
+#include <sys/types.h>
+#include <MESA/MESA_prof_load.h>
+#include <MESA/MESA_handle_logger.h>
+
+void *__ftp_decoder_logger_handle;
+
+static void ftp_msg_free_cb(struct session *sess, void *msg, void *msg_free_arg)
+{
+ free(msg);
+}
+
+static void *ftp_context_ctx_new_cb(struct session *sess, void *plugin_env)
+{
+ struct ftp_decoder_env *fenv = (struct ftp_decoder_env *)plugin_env;
+ size_t payload_len;
+ const char *payload = session_get0_current_payload(sess, &payload_len);
+ int curdir = ftp_session_get_pkt_dir(sess);
+ int thread_idx = session_get_current_thread_id(sess);
+ int is_ctrl_link = ftp_ctrl_identify(sess, payload, payload_len, curdir);
+ if (is_ctrl_link)
+ {
+ struct ftp_context *fctx = (struct ftp_context *)calloc(1, sizeof(struct ftp_context));
+ fctx->link_type = FTP_LINK_CTRL;
+ ftp_decoder_stat_incrby(thread_idx, fenv, FTPD_STAT_CTRL_LINK_OPEN, 1);
+ ftp_runtime_log(RLOG_LV_DEBUG, "ftp ctrl link new: %s", session_get0_readable_addr(sess));
+ return fctx;
+ }
+ ftp_runtime_log(RLOG_LV_DEBUG, "ftp_data_identify: is not ctrl link: '%s' ", session_get0_readable_addr(sess));
+
+ struct ftp_context *data_ctx = ftp_data_identify(sess, fenv);
+ if (data_ctx)
+ {
+ data_ctx->link_type = FTP_LINK_DATA;
+ ftp_decoder_stat_incrby(thread_idx, fenv, FTPD_STAT_DATA_LINK_OPEN, 1);
+ ftp_runtime_log(RLOG_LV_DEBUG, "ftp data link new: '%s' ", session_get0_readable_addr(sess));
+ return data_ctx;
+ }
+ ftp_runtime_log(RLOG_LV_DEBUG, "ftp_data_identify: is not data link: '%s' ", session_get0_readable_addr(sess));
+ stellar_session_plugin_dettach_current_session(sess);
+ return NULL;
+}
+
+void ftp_context_ctx_free_cb(struct session *sess, void *session_ctx, void *plugin_env)
+{
+ struct ftp_context *fctx = (struct ftp_context *)session_ctx;
+ if (NULL == fctx)
+ {
+ return;
+ }
+ int thread_idx = session_get_current_thread_id(sess);
+ if (fctx->link_type == FTP_LINK_CTRL)
+ {
+ ftp_decoder_stat_incrby(thread_idx, (struct ftp_decoder_env *)plugin_env, FTPD_STAT_CTRL_LINK_CLOSE, 1);
+ }
+ else
+ {
+ ftp_decoder_stat_incrby(thread_idx, (struct ftp_decoder_env *)plugin_env, FTPD_STAT_DATA_LINK_CLOSE, 1);
+ }
+ ftp_runtime_log(RLOG_LV_DEBUG, "ftp_context_ctx_free_cb: %s", session_get0_readable_addr(sess));
+ ftp_parse_result_free(&fctx->parse_result);
+ free(fctx);
+}
+
+static void ftpd_logger_init(struct ftp_decoder_env *fenv)
+{
+ int log_level;
+ char log_path[256];
+ MESA_load_profile_int_def(FTP_DECODER_CFG_FILE, "FTP", "log_level", &log_level, 30);
+ MESA_load_profile_string_def(FTP_DECODER_CFG_FILE, "FTP", "log_path", log_path, sizeof(log_path), "./log/ftp_decoder.log");
+ MESA_handle_runtime_log_creation("./etc/sapp_log.conf");
+ fenv->logger_handle = MESA_create_runtime_log_handle(log_path, log_level);
+ __ftp_decoder_logger_handle = fenv->logger_handle;
+ return;
+}
+
+void ftp_message_reset_iterator(const struct ftp_message *msg)
+{
+ msg->fctx->parse_result.cursor = 0;
+}
+
+static void ftp_ctrl_entry(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv)
+{
+ size_t payload_len;
+ const char *payload = session_get0_current_payload(sess, &payload_len);
+ int curdir = ftp_session_get_pkt_dir(sess);
+ if (NULL == payload || payload_len == 0)
+ {
+ return;
+ }
+ int thread_idx = session_get_current_thread_id(sess);
+ ftp_cmd_readline(&fctx->cmd_result, payload, payload_len);
+ ftp_cmd_process(sess, fctx, fenv, curdir);
+ if (curdir == PACKET_DIRECTION_C2S)
+ {
+ ftp_decoder_stat_incrby(thread_idx, fenv, FTPD_STAT_CTRL_LINK_BYTES_C2S, (long long)payload_len);
+ }
+ else
+ {
+ ftp_decoder_stat_incrby(thread_idx, fenv, FTPD_STAT_CTRL_LINK_BYTES_S2C, (long long)payload_len);
+ }
+}
+
+static void ftp_data_entry(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv)
+{
+ enum session_state sstate = session_get_current_state(sess);
+ if (SESSION_STATE_OPENING == sstate)
+ {
+ memset(fctx->parse_result.push_result_flags, 0, sizeof(fctx->parse_result.push_result_flags));
+ ftp_runtime_log(RLOG_LV_DEBUG, "ftp data link opening: %s", session_get0_readable_addr(sess));
+ }
+
+ size_t payload_len;
+ const char *payload = session_get0_current_payload(sess, &payload_len);
+ if (NULL == payload || payload_len == 0)
+ {
+ return;
+ }
+ enum ftp_msg_type dlink_presentation = fctx->data_link_presentation;
+ if (FTP_INVENTORY != dlink_presentation && FTP_FILE_CONTENT != dlink_presentation)
+ {
+ dlink_presentation = FTP_FILE_CONTENT; // unknown data link type, default as FTP_FILE_CONTENT
+ }
+ fctx->parse_result.result_array[dlink_presentation].iov_base = (void *)payload;
+ fctx->parse_result.result_array[dlink_presentation].iov_len = payload_len;
+ fctx->parse_result.push_result_flags[dlink_presentation] = 0;
+
+ ftp_decoder_push_msg(sess, fctx, fenv);
+ int thread_idx = session_get_current_thread_id(sess);
+ int curdir = ftp_session_get_pkt_dir(sess);
+ if (curdir == PACKET_DIRECTION_C2S)
+ {
+ ftp_decoder_stat_incrby(thread_idx, fenv, FTPD_STAT_DATA_LINK_BYTES_C2S, (long long)payload_len);
+ }
+ else
+ {
+ ftp_decoder_stat_incrby(thread_idx, fenv, FTPD_STAT_DATA_LINK_BYTES_S2C, (long long)payload_len);
+ }
+}
+
+static void ftp_decoder_entry(struct session *sess, int topic_id, const void *msg, void *per_session_ctx, void *plugin_env)
+{
+ struct ftp_context *fctx = (struct ftp_context *)per_session_ctx;
+ if (FTP_LINK_CTRL == fctx->link_type)
+ {
+ ftp_ctrl_entry(sess, fctx, (struct ftp_decoder_env *)plugin_env);
+ }
+ else
+ {
+ ftp_data_entry(sess, fctx, (struct ftp_decoder_env *)plugin_env);
+ }
+}
+
+int ftp_message_iterate(const struct ftp_message *msg, enum ftp_msg_type *type, struct iovec *value)
+{
+ if (msg->fctx->parse_result.cursor >= FTP_MSG_MAX)
+ {
+ return 0;
+ }
+ int index = msg->fctx->parse_result.cursor;
+ for (int i = index; i < (int)FTP_MSG_MAX; i++)
+ {
+ if (msg->fctx->parse_result.push_result_flags[i] == 1)
+ {
+ continue;
+ }
+ if (msg->fctx->parse_result.result_array[i].iov_base == NULL || msg->fctx->parse_result.result_array[i].iov_len == 0)
+ {
+ continue;
+ }
+ *type = (enum ftp_msg_type)i;
+ value->iov_base = msg->fctx->parse_result.result_array[i].iov_base;
+ value->iov_len = msg->fctx->parse_result.result_array[i].iov_len;
+ msg->fctx->parse_result.push_result_flags[i] = 1;
+ msg->fctx->parse_result.cursor = index + 1;
+ return 1;
+ }
+ return 0;
+}
+
+int ftp_decoder_push_msg(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv)
+{
+ struct ftp_message *fmsg = (struct ftp_message *)calloc(1, sizeof(struct ftp_message));
+ fmsg->fctx = fctx;
+ fctx->parse_result.cursor = 0;
+ if (FTP_LINK_CTRL == fctx->link_type)
+ {
+ session_mq_publish_message(sess, fenv->ftp_pub_ctrl_topic_id, fmsg);
+ }
+ else
+ {
+ session_mq_publish_message(sess, fenv->ftp_pub_data_topic_id, fmsg);
+ }
+ return 0;
+}
+
+extern "C" void *FTP_ONLOAD(struct stellar *st)
+{
+ struct ftp_decoder_env *fenv = (struct ftp_decoder_env *)calloc(1, sizeof(struct ftp_decoder_env));
+
+ fenv->thread_count = stellar_get_worker_thread_num(st);
+ assert(fenv->thread_count >= 1);
+ fenv->plugin_id = stellar_session_plugin_register(st, ftp_context_ctx_new_cb, ftp_context_ctx_free_cb, fenv);
+ fenv->tcp_sub_topic_id = stellar_session_mq_get_topic_id(st, TOPIC_TCP_STREAM);
+ stellar_session_mq_subscribe(st, fenv->tcp_sub_topic_id, ftp_decoder_entry, fenv->plugin_id);
+ fenv->ftp_pub_ctrl_topic_id = stellar_session_mq_create_topic(st, FTP_DECODER_CTRL_TOPIC, ftp_msg_free_cb, fenv);
+ fenv->ftp_pub_data_topic_id = stellar_session_mq_create_topic(st, FTP_DECODER_DATA_TOPIC, ftp_msg_free_cb, fenv);
+ ftp_hash_table_create(fenv);
+ ftp_decoder_stat_init(fenv);
+ ftpd_logger_init(fenv);
+ ftp_runtime_log(RLOG_LV_DEBUG, "FTP_ONLOAD success.");
+ return fenv;
+}
+
+extern "C" void FTP_UNLOAD(void *plugin_env)
+{
+ struct ftp_decoder_env *fenv = (struct ftp_decoder_env *)plugin_env;
+ ftp_runtime_log(RLOG_LV_DEBUG, "FTP_UNLOAD ...");
+ ftp_hash_table_destroy(fenv);
+ ftp_decoder_stat_free(fenv);
+ __ftp_decoder_logger_handle = NULL;
+ MESA_destroy_runtime_log_handle(fenv->logger_handle);
+ free(fenv);
+} \ No newline at end of file
diff --git a/src/ftp_decoder_hash.cpp b/src/ftp_decoder_hash.cpp
new file mode 100644
index 0000000..752505c
--- /dev/null
+++ b/src/ftp_decoder_hash.cpp
@@ -0,0 +1,169 @@
+#include "ftp_decoder_inner.h"
+#include "ftp_decoder_hash.h"
+#include "ftp_decoder_util.h"
+#include <MESA_htable.h>
+#include <cstdint>
+#include <arpa/inet.h>
+
+static thread_local char __ftp_hash_string_buf[1024];
+
+void ftp_declare_datalink_free(void *data)
+{
+ struct ftp_context *fctx = (struct ftp_context *)data;
+ ftp_parse_result_free(&fctx->parse_result);
+ free(fctx);
+}
+
+static void ftp_declare_datalink_free_only_key(void *data)
+{
+ return; // do nothing
+}
+
+int ftp_hash_add(void *handle, const void *key, u_int32_t key_len, void *value)
+{
+ if (MESA_htable_add(handle, (const uint8_t *)key, key_len, value) < 0)
+ {
+ ftp_declare_datalink_free(value);
+ return -1;
+ }
+ return 0;
+}
+
+void ftp_hash_del(void *handle, const struct ftp_link_key *hkey)
+{
+ const void *key;
+ u_int32_t key_len;
+ if (SESSION_ADDR_TYPE_IPV4_TCP == hkey->addr_type || SESSION_ADDR_TYPE_IPV4_UDP == hkey->addr_type)
+ {
+ key = &hkey->tuplev4;
+ key_len = sizeof(struct session_addr_ipv4);
+ }
+ else
+ {
+ key = &hkey->tuplev6;
+ key_len = sizeof(struct session_addr_ipv6);
+ }
+ MESA_htable_del(handle, (const uint8_t *)key, key_len, ftp_declare_datalink_free_only_key);
+}
+
+void ftp_make_hkey_v4(struct session_addr_ipv4 *keyv4, uint32_t sip_net, uint32_t dip_net, uint16_t dip_port_net)
+{
+ memset(keyv4, 0, sizeof(struct session_addr_ipv4));
+ keyv4->saddr = sip_net;
+ keyv4->daddr = dip_net;
+ keyv4->sport = 0;
+ keyv4->dport = dip_port_net;
+}
+
+void ftp_make_hkey_v6(struct session_addr_ipv6 *keyv6, const struct in6_addr *sip, const struct in6_addr *dip, uint16_t dip_port_net)
+{
+ memset(keyv6, 0, sizeof(struct session_addr_ipv6));
+ memcpy(keyv6->saddr, sip, IPV6_ADDR_LEN);
+ memcpy(keyv6->daddr, dip, IPV6_ADDR_LEN);
+ keyv6->sport = 0;
+ keyv6->dport = dip_port_net;
+}
+
+int ftp_build_hashkey_from_session(struct session *sess, struct ftp_link_key *hkey)
+{
+ enum session_addr_type saddr_type;
+ struct session_addr *saddr = session_get0_addr(sess, &saddr_type);
+ hkey->addr_type = saddr_type;
+ if (SESSION_ADDR_TYPE_IPV4_TCP == saddr_type)
+ {
+ ftp_make_hkey_v4(&hkey->tuplev4, saddr->ipv4.saddr, saddr->ipv4.daddr, saddr->ipv4.dport);
+ }
+ else if (SESSION_ADDR_TYPE_IPV6_TCP == saddr_type)
+ {
+ ftp_make_hkey_v6(&hkey->tuplev6, (struct in6_addr *)saddr->ipv6.saddr, (struct in6_addr *)saddr->ipv6.daddr, saddr->ipv6.dport);
+ }
+ else
+ {
+ return -1;
+ }
+ return 0;
+}
+
+void *ftp_hash_search(void *handle, const struct ftp_link_key *hkey)
+{
+ const void *key;
+ u_int32_t key_len;
+ if (SESSION_ADDR_TYPE_IPV4_TCP == hkey->addr_type || SESSION_ADDR_TYPE_IPV4_UDP == hkey->addr_type)
+ {
+ key = &hkey->tuplev4;
+ key_len = sizeof(struct session_addr_ipv4);
+ }
+ else
+ {
+ key = &hkey->tuplev6;
+ key_len = sizeof(struct session_addr_ipv6);
+ }
+ return MESA_htable_search(handle, (const uint8_t *)key, key_len);
+}
+
+const char *ftp_hash_key_to_str(const struct ftp_link_key *hkey)
+{
+ char sip_str[INET6_ADDRSTRLEN], dip_str[INET6_ADDRSTRLEN];
+ unsigned short sport_host, dport_host;
+ if (SESSION_ADDR_TYPE_IPV4_TCP == hkey->addr_type || SESSION_ADDR_TYPE_IPV4_UDP == hkey->addr_type)
+ {
+ inet_ntop(AF_INET, &hkey->tuplev4.saddr, sip_str, INET_ADDRSTRLEN);
+ inet_ntop(AF_INET, &hkey->tuplev4.daddr, dip_str, INET_ADDRSTRLEN);
+ sport_host = ntohs(hkey->tuplev4.sport);
+ dport_host = ntohs(hkey->tuplev4.dport);
+ }
+ else
+ {
+ inet_ntop(AF_INET, hkey->tuplev6.saddr, sip_str, INET6_ADDRSTRLEN);
+ inet_ntop(AF_INET, hkey->tuplev6.daddr, dip_str, INET6_ADDRSTRLEN);
+ sport_host = ntohs(hkey->tuplev6.sport);
+ dport_host = ntohs(hkey->tuplev6.dport);
+ }
+ snprintf(__ftp_hash_string_buf, sizeof(__ftp_hash_string_buf), "%s:%u -> %s:%u", sip_str, sport_host, dip_str, dport_host);
+ return __ftp_hash_string_buf;
+}
+
+int ftp_hash_table_create(struct ftp_decoder_env *fenv)
+{
+ int opt, opt_len;
+ fenv->data_link_table = (void **)calloc(fenv->thread_count, sizeof(void *));
+ for (int i = 0; i < fenv->thread_count; i++)
+ {
+ fenv->data_link_table[i] = MESA_htable_born();
+ assert(fenv->data_link_table[i]);
+
+ opt = 4096;
+ opt_len = sizeof(int);
+ MESA_htable_set_opt(fenv->data_link_table[i], MHO_HASH_SLOT_SIZE, &opt, opt_len);
+
+ opt = 0;
+ opt_len = sizeof(int);
+ MESA_htable_set_opt(fenv->data_link_table[i], MHO_THREAD_SAFE, &opt, opt_len);
+
+ opt = 0;
+ opt_len = sizeof(int);
+ MESA_htable_set_opt(fenv->data_link_table[i], MHO_SCREEN_PRINT_CTRL, &opt, opt_len);
+
+ opt = 10;
+ opt_len = sizeof(int);
+ MESA_htable_set_opt(fenv->data_link_table[i], MHO_EXPIRE_TIME, &opt, opt_len);
+ MESA_htable_set_opt(fenv->data_link_table[i], MHO_CBFUN_DATA_FREE, (void *)&ftp_declare_datalink_free, sizeof(void *));
+ MESA_htable_mature(fenv->data_link_table[i]);
+ }
+ return 0;
+}
+
+void ftp_hash_table_destroy(struct ftp_decoder_env *fenv)
+{
+ unsigned int remain_items;
+ for (int i = 0; i < fenv->thread_count; i++)
+ {
+ remain_items = MESA_htable_get_elem_num(fenv->data_link_table[0]);
+ if (remain_items > 0)
+ {
+ ftp_runtime_log(RLOG_LV_DEBUG, "ftp_hash_table_destroy(): thread:%d declare datalink table remain items:%u\n", i, remain_items);
+ }
+ MESA_htable_destroy(fenv->data_link_table[i], ftp_declare_datalink_free);
+ }
+ free(fenv->data_link_table);
+} \ No newline at end of file
diff --git a/src/ftp_decoder_hash.h b/src/ftp_decoder_hash.h
new file mode 100644
index 0000000..21a702f
--- /dev/null
+++ b/src/ftp_decoder_hash.h
@@ -0,0 +1,38 @@
+#pragma once
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+#include <stellar/session.h>
+#include <stellar/session_exdata.h>
+#include <stellar/session_mq.h>
+#include <stellar/stellar.h>
+#ifdef __cplusplus
+}
+#endif
+#include <stddef.h>
+#include <MESA/MESA_htable.h>
+
+/*
+ for ftp passive mode, source port is random, so we can't use tuple4 to identify data link.
+ so always set source port to 0!
+*/
+struct ftp_link_key
+{
+ enum session_addr_type addr_type;
+ int __pad__;
+ union
+ {
+ struct session_addr_ipv4 tuplev4;
+ struct session_addr_ipv6 tuplev6;
+ };
+} __attribute__((packed));
+
+int ftp_hash_add(void *handle, const void *key, u_int32_t key_len, void *value);
+void *ftp_hash_search(void *handle, const struct ftp_link_key *hkey);
+void ftp_hash_del(void *handle, const struct ftp_link_key *hkey);
+void ftp_declare_datalink_free(void *data);
+void ftp_make_hkey_v4(struct session_addr_ipv4 *keyv4, uint32_t sip_net, uint32_t dip_net, uint16_t dip_port_net);
+void ftp_make_hkey_v6(struct session_addr_ipv6 *keyv6, const struct in6_addr *sip, const struct in6_addr *dip, uint16_t dip_port_net);
+const char *ftp_hash_key_to_str(const struct ftp_link_key *hkey);
+int ftp_build_hashkey_from_session(struct session *sess, struct ftp_link_key *hkey); \ No newline at end of file
diff --git a/src/ftp_decoder_inner.h b/src/ftp_decoder_inner.h
new file mode 100644
index 0000000..9f632a1
--- /dev/null
+++ b/src/ftp_decoder_inner.h
@@ -0,0 +1,125 @@
+#pragma once
+#include <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <string.h>
+#include "ftp_decoder.h"
+#include <MESA/MESA_handle_logger.h>
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+#include <stellar/session.h>
+#include <stellar/session_exdata.h>
+#include <stellar/session_mq.h>
+#include <stellar/stellar.h>
+#ifdef __cplusplus
+}
+#endif
+#include "ftp_decoder_stat.h"
+#include "ftp_decoder_hash.h"
+
+#define FTPD_IDENTIRY_MIN_LEN 4
+#define FTPD_IDENTIRY_MAX_LEN 32
+#define FTPD_CMD_MAX_LENGTH 128
+#define FTP_DECODER_CFG_FILE "./conf/ftp_decoder/ftp.conf"
+#define FTP_DECODER_FIELDSTAT_NAME "ftp_decoder_statistics"
+#define FTP_DECODER_FIELDSTAT_OUTPUT_FILE "./metrics/ftp_decoder_fs4.json"
+#define FTP_DECODER_FIELDSTAT_OUTPUT_INTERVAL 3
+
+extern void *__ftp_decoder_logger_handle;
+#define ftp_runtime_log(log_level, format, ...) \
+ do \
+ { \
+ if (MESA_handle_runtime_log_level_enabled(__ftp_decoder_logger_handle, log_level)) \
+ { \
+ MESA_handle_runtime_log(__ftp_decoder_logger_handle, log_level, "ftp_decoder", format, ##__VA_ARGS__); \
+ } \
+ } while (0)
+
+#define IOVEC_PRINT(iov) (int)(iov).iov_len, (char *)(iov).iov_base
+#define IOVEC_PRINT_PTR(iov_p) (int)(iov_p->iov_len), (char *)(iov_p->iov_base)
+#ifndef fstring
+typedef struct iovec fstring;
+#endif
+enum ftp_link_type
+{
+ FTP_LINK_CTRL,
+ FTP_LINK_DATA,
+};
+
+enum ftp_data_link_type
+{
+ FTP_DATA_LINK_FILE,
+ FTP_DATA_LINK_INVENTORY,
+};
+
+struct ftp_interact_line
+{
+ fstring cmd_refer; // pointer to packet payload
+ fstring arg_refer; // pointer to packet payload
+};
+
+struct ftp_parse_result
+{
+ /* all data is persistent, sync with data link, need be free when session close */
+ int cursor;
+ struct iovec result_array[FTP_MSG_MAX];
+ uint8_t push_result_flags[FTP_MSG_MAX];
+};
+
+struct ftp_context
+{
+ enum ftp_link_type link_type;
+ enum ftp_msg_type data_link_presentation; // FTP_INVENTORY or FTP_FILE_CONTENT
+ struct ftp_interact_line cmd_result;
+ struct ftp_parse_result parse_result;
+ struct ftp_interact_line last_cmd;
+ struct ftp_link_key last_data_link_key;
+};
+
+struct ftp_interact_parser
+{
+ enum ftp_msg_type cmd_msg_type;
+ const char *cmd_name;
+ int cmd_len;
+ int (*cmd_handler)(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv);
+};
+
+struct ftp_stat
+{
+ struct fieldstat_easy *fs4_instance;
+ int fs4_counter_id[FTPD_STAT_MAX];
+};
+
+struct ftp_decoder_env
+{
+ void *logger_handle;
+ int plugin_id;
+ int thread_count;
+ int tcp_sub_topic_id;
+ int ftp_pub_ctrl_topic_id;
+ int ftp_pub_data_topic_id;
+ void **data_link_table; // key is tuple3 for PASV, tuple4 for PORT
+ struct ftp_stat fstat;
+};
+
+struct ftp_message
+{
+ struct ftp_context *fctx;
+};
+
+extern "C" void *FTP_ONLOAD(struct stellar *st);
+extern "C" void FTP_UNLOAD(void *plugin_env);
+int ftp_hash_table_create(struct ftp_decoder_env *fenv);
+void ftp_hash_table_destroy(struct ftp_decoder_env *fenv);
+int ftp_ctrl_identify_by_payload(const char *payload, size_t len, u_int8_t curdir);
+int ftp_ctrl_identify_by_addr(struct session *sess);
+int ftp_ctrl_identify(struct session *sess, const char *payload, size_t len, u_int8_t curdir);
+struct ftp_context *ftp_data_identify(struct session *sess, struct ftp_decoder_env *fenv);
+int ftp_cmd_readline(struct ftp_interact_line *line, const char *payload, size_t len);
+int ftp_cmd_process(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv, int curdir);
+int ftp_decoder_push_msg(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv);
+int ftp_parse_ipv4_port_style(const fstring *cmd_str, unsigned int *ipv4_net, unsigned short *port_net);
+int ftp_parse_ipv6_port_style(const fstring *cmd_str, unsigned short *port_net);
+int ftp_parse_eprt_ipport_style(const fstring *arg_str, struct in6_addr *ipd_addr, unsigned short *port_net); \ No newline at end of file
diff --git a/src/ftp_decoder_proto.cpp b/src/ftp_decoder_proto.cpp
new file mode 100644
index 0000000..dd5f14a
--- /dev/null
+++ b/src/ftp_decoder_proto.cpp
@@ -0,0 +1,566 @@
+#include "ftp_decoder_inner.h"
+#include "ftp_decoder.h"
+#include "ftp_decoder_util.h"
+#include "ftp_decoder_hash.h"
+#include <sys/types.h>
+#include <arpa/inet.h>
+
+/*
+ -------------
+ |/---------\|
+ || User || --------
+ ||Interface|<--->| User |
+ |\----^----/| --------
+ ---------- | | |
+ |/------\| FTP Commands |/----V----\|
+ ||Server|<---------------->| User ||
+ || PI || FTP Replies || PI ||
+ |\--^---/| |\----^----/|
+ | | | | | |
+ -------- |/--V---\| Data |/----V----\| --------
+ | File |<--->|Server|<---------------->| User |<--->| File |
+ |System| || DTP || Connection || DTP || |System|
+ -------- |\------/| |\---------/| --------
+ ---------- -------------
+
+ Server-FTP USER-FTP
+*/
+
+/*
+ ctrl link command data link tcp/ip stack event
+---------------------------------------------------------------------------------------------------
+ PORT 192,168,38,2,202,95
+ listen tcp port: 51807
+ 200 PORT command successful.
+ LIST
+ server use src tcp port 20 connecting to client tcp port 51807
+ data connection is established
+ send 'ls -l' result to cliet
+ 226 Directory send OK.
+ close the tcp data connection
+ waiting for next command...
+*/
+static const struct iovec G_FTP_EMPTY_IOV = {NULL, 0};
+
+static struct ftp_context *ftp_get_declare_datalink_ctx(struct session *sess, struct ftp_context *ctrl_fctx, struct ftp_decoder_env *fenv)
+{
+ int thread_id = session_get_current_thread_id(sess);
+ struct ftp_context *declare_data_link_ftx = (struct ftp_context *)ftp_hash_search(fenv->data_link_table[thread_id], &ctrl_fctx->last_data_link_key);
+ if (NULL == declare_data_link_ftx)
+ {
+ ftp_runtime_log(RLOG_LV_FATAL, "declare data link not found, key=%s", ftp_hash_key_to_str(&ctrl_fctx->last_data_link_key));
+ return NULL;
+ }
+ return declare_data_link_ftx;
+}
+
+static void ftp_assemble_uri(struct session *sess, const struct iovec *raw_arg, struct iovec *uri)
+{
+ char dip_str[INET6_ADDRSTRLEN] = {};
+ char tmp_url[2048] = {};
+ enum session_addr_type saddr_type;
+ struct session_addr *saddr = session_get0_addr(sess, &saddr_type);
+ if (saddr_type == SESSION_ADDR_TYPE_IPV4_TCP)
+ {
+ inet_ntop(AF_INET, &saddr->ipv4.daddr, dip_str, INET_ADDRSTRLEN);
+ }
+ else
+ {
+ inet_ntop(AF_INET6, saddr->ipv6.daddr, dip_str, INET6_ADDRSTRLEN);
+ }
+ int ret = snprintf(tmp_url, sizeof(tmp_url), "ftp://%s/%.*s", dip_str, IOVEC_PRINT(*raw_arg));
+ if (uri->iov_base)
+ {
+ free(uri->iov_base);
+ }
+ uri->iov_base = calloc(1, ret);
+ memcpy(uri->iov_base, tmp_url, ret);
+ uri->iov_len = ret;
+}
+
+int ftp_cmd_handler_do_user(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv)
+{
+ fstring_safe_dup(&fctx->cmd_result.arg_refer, &fctx->parse_result.result_array[FTP_ACCOUNT]);
+ ftp_runtime_log(RLOG_LV_DEBUG, "'%s': USER: %.*s", session_get0_readable_addr(sess), IOVEC_PRINT(fctx->parse_result.result_array[FTP_ACCOUNT]));
+ return 0;
+}
+
+int ftp_cmd_handler_do_pass(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv)
+{
+ fstring_safe_dup(&fctx->cmd_result.arg_refer, &fctx->parse_result.result_array[FTP_PASSWORD]);
+ ftp_runtime_log(RLOG_LV_DEBUG, "'%s': PASS: %.*s", session_get0_readable_addr(sess), IOVEC_PRINT(fctx->parse_result.result_array[FTP_PASSWORD]));
+ return 0;
+}
+
+int ftp_parse_ipv4_port_style(const fstring *cmd_str, unsigned int *ipv4_net, unsigned short *port_net)
+{
+ unsigned int fields[6];
+ char raw_cmd_str_tmp[cmd_str->iov_len + 1] = {};
+ memcpy(raw_cmd_str_tmp, cmd_str->iov_base, cmd_str->iov_len);
+ char only_integer_str_tmp[cmd_str->iov_len + 1] = {};
+ if (sscanf(raw_cmd_str_tmp, "%*[a-zA-Z (]%[0-9,]", only_integer_str_tmp) <= 0)
+ {
+ if (sscanf(raw_cmd_str_tmp, "%[1234567890,]", only_integer_str_tmp) <= 0)
+ {
+ ftp_runtime_log(RLOG_LV_FATAL, "ftp_parse_ipv4_port_style parse error: %.*s", IOVEC_PRINT_PTR(cmd_str));
+ return -1;
+ }
+ }
+ int ret = sscanf(only_integer_str_tmp, "%u,%u,%u,%u,%u,%u", &fields[0], &fields[1], &fields[2], &fields[3], &fields[4], &fields[5]);
+ if (ret != 6)
+ {
+ ftp_runtime_log(RLOG_LV_FATAL, "ftp_parse_ipv4_port_style parse error: %.*s", cmd_str->iov_len, only_integer_str_tmp);
+ return -1;
+ }
+ unsigned int dst_ip_host = (fields[0] << 24) | (fields[1] << 16) | (fields[2] << 8) | fields[3];
+ unsigned short dst_port_host = (fields[4] << 8) | fields[5];
+ *ipv4_net = htonl(dst_ip_host);
+ *port_net = htons(dst_port_host);
+ return 0;
+}
+/* PORT command only support IPv4, arg pattern: h1,h2,h3,h4,p1,p2 */
+int ftp_cmd_handler_do_port(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv)
+{
+ fstring_safe_dup(&fctx->cmd_result.cmd_refer, &fctx->parse_result.result_array[FTP_TRANS_MODE]);
+ fctx->parse_result.push_result_flags[FTP_TRANS_MODE] = 0;
+
+ unsigned int dst_ip_net;
+ unsigned short dst_port_net;
+ if (ftp_parse_ipv4_port_style(&fctx->cmd_result.arg_refer, &dst_ip_net, &dst_port_net) < 0)
+ {
+ return -1;
+ }
+ enum session_addr_type saddr_type;
+ struct session_addr *saddr = session_get0_addr(sess, &saddr_type);
+ fctx->last_data_link_key.addr_type = SESSION_ADDR_TYPE_IPV4_TCP;
+ // in active mode, new data link src ip address is server ip address
+ ftp_make_hkey_v4(&fctx->last_data_link_key.tuplev4, saddr->ipv4.daddr, dst_ip_net, dst_port_net);
+
+ struct ftp_context *new_data_link_ctx = ftp_decoder_context_deep_dup(fctx);
+ int thread_id = session_get_current_thread_id(sess);
+ ftp_hash_add(fenv->data_link_table[thread_id], &fctx->last_data_link_key.tuplev4, sizeof(struct session_addr_ipv4), new_data_link_ctx);
+
+ ftp_decoder_stat_incrby(thread_id, fenv, FTPD_STAT_NEGOTIATE_DATA_LINK, 1);
+
+ ftp_runtime_log(RLOG_LV_DEBUG, "do_port_cb: %.*s, parsed dip:%x, dport:%u", IOVEC_PRINT(fctx->parse_result.result_array[FTP_TRANS_MODE]), dst_ip_net, dst_port_net);
+ return 0;
+}
+
+/* PASV command only support IPv4, the response like: '227 Entering Passive Mode (218,13,32,6,78,40).' */
+int ftp_cmd_handler_do_pasv(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv)
+{
+ fstring_safe_dup(&fctx->cmd_result.cmd_refer, &fctx->parse_result.result_array[FTP_TRANS_MODE]);
+ fctx->parse_result.push_result_flags[FTP_TRANS_MODE] = 0;
+ ftp_runtime_log(RLOG_LV_DEBUG, "'%s' PASV command", session_get0_readable_addr(sess));
+ return 0;
+}
+
+int ftp_cmd_handler_do_list(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv)
+{
+ fstring_safe_dup(&fctx->cmd_result.cmd_refer, &fctx->parse_result.result_array[FTP_TRANS_DIR]);
+ fstring_safe_dup(&G_FTP_EMPTY_IOV, &fctx->parse_result.result_array[FTP_URI]);
+ struct ftp_context *declare_data_link_ftx = ftp_get_declare_datalink_ctx(sess, fctx, fenv);
+ if (declare_data_link_ftx)
+ {
+ fstring_safe_dup(&fctx->cmd_result.cmd_refer, &declare_data_link_ftx->parse_result.result_array[FTP_TRANS_DIR]);
+ fstring_safe_dup(&G_FTP_EMPTY_IOV, &declare_data_link_ftx->parse_result.result_array[FTP_URI]);
+ declare_data_link_ftx->data_link_presentation = FTP_INVENTORY;
+ }
+ ftp_runtime_log(RLOG_LV_DEBUG, "'%s': LIST command", session_get0_readable_addr(sess));
+ return 0;
+}
+
+static void ftp_cmd_handler_do_stor_retr_common(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv)
+{
+ fstring_safe_dup(&fctx->cmd_result.cmd_refer, &fctx->parse_result.result_array[FTP_TRANS_DIR]);
+ ftp_assemble_uri(sess, &fctx->cmd_result.arg_refer, &fctx->parse_result.result_array[FTP_URI]);
+ fctx->parse_result.push_result_flags[FTP_TRANS_DIR] = 0;
+ fctx->parse_result.push_result_flags[FTP_URI] = 0;
+
+ struct ftp_context *declare_data_link_ftx = ftp_get_declare_datalink_ctx(sess, fctx, fenv);
+ if (declare_data_link_ftx)
+ {
+ declare_data_link_ftx->data_link_presentation = FTP_FILE_CONTENT;
+ fstring_safe_dup(&fctx->parse_result.result_array[FTP_TRANS_DIR], &declare_data_link_ftx->parse_result.result_array[FTP_TRANS_DIR]);
+ fstring_safe_dup(&fctx->parse_result.result_array[FTP_URI], &declare_data_link_ftx->parse_result.result_array[FTP_URI]);
+ // ftp_update_parse_result(declare_data_link_ftx, &fctx->parse_result.result_array[FTP_URI], FTP_URI);
+ }
+}
+
+int ftp_cmd_handler_do_stor(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv)
+{
+ ftp_cmd_handler_do_stor_retr_common(sess, fctx, fenv);
+ ftp_runtime_log(RLOG_LV_DEBUG, "'%s', STOR uri:%.*s", session_get0_readable_addr(sess), IOVEC_PRINT(fctx->parse_result.result_array[FTP_URI]));
+ return 0;
+}
+
+int ftp_cmd_handler_do_retr(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv)
+{
+ ftp_cmd_handler_do_stor_retr_common(sess, fctx, fenv);
+ ftp_runtime_log(RLOG_LV_DEBUG, "'%s', RETR uri:%.*s", session_get0_readable_addr(sess), IOVEC_PRINT(fctx->parse_result.result_array[FTP_URI]));
+ return 0;
+}
+
+int ftp_parse_eprt_ipport_style(const fstring *arg_str, struct in6_addr *ipd_addr, unsigned short *port_net)
+{
+ unsigned int port_host;
+ int inet_proto;
+ char ip6_addr_str[INET6_ADDRSTRLEN] = {};
+ char raw_cmd_str_tmp[arg_str->iov_len + 1] = {};
+ memcpy(raw_cmd_str_tmp, arg_str->iov_base, arg_str->iov_len);
+
+ char *save_ptr, *ptr;
+ const char *delim = " |\t";
+ ptr = strtok_r(raw_cmd_str_tmp, delim, &save_ptr);
+ if (NULL == ptr)
+ {
+ ftp_runtime_log(RLOG_LV_FATAL, "ftp_parse_eprt_ipport_style parse error: %.*s", IOVEC_PRINT_PTR(arg_str));
+ return -1;
+ }
+ inet_proto = atoi(ptr);
+ if (2 != inet_proto)
+ {
+ ftp_runtime_log(RLOG_LV_FATAL, "ftp_parse_eprt_ipport_style parse error: %.*s, not support inet: %d", IOVEC_PRINT_PTR(arg_str), inet_proto);
+ return -1;
+ }
+ ptr = strtok_r(NULL, delim, &save_ptr);
+ if (NULL == ptr)
+ {
+ ftp_runtime_log(RLOG_LV_FATAL, "ftp_parse_eprt_ipport_style parse error: %.*s", IOVEC_PRINT_PTR(arg_str));
+ return -1;
+ }
+ strncpy(ip6_addr_str, ptr, INET6_ADDRSTRLEN - 1);
+ ptr = strtok_r(NULL, delim, &save_ptr);
+ if (NULL == ptr)
+ {
+ ftp_runtime_log(RLOG_LV_FATAL, "ftp_parse_eprt_ipport_style parse error: %.*s", IOVEC_PRINT_PTR(arg_str));
+ return -1;
+ }
+ port_host = (unsigned int)atoi(ptr);
+
+ while (strtok_r(NULL, "|", &save_ptr))
+ ;
+ inet_pton(AF_INET6, ip6_addr_str, ipd_addr);
+ *port_net = htons((unsigned short)port_host);
+ return 0;
+}
+
+/* EPRT support IPv4 and IPv6, pattern: EPRT<space><d><net-prt><d><net-addr><d><tcp-port><d>
+ example:
+ EPRT |1|132.235.1.2|6275|
+ EPRT |2|1080::8:800:200C:417A|5282|
+ refer: https://datatracker.ietf.org/doc/html/rfc2428#section-2
+*/
+int ftp_cmd_handler_do_eprt(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv)
+{
+ struct in6_addr ipd_addr;
+ unsigned short port_net;
+ fstring_safe_dup(&fctx->cmd_result.cmd_refer, &fctx->parse_result.result_array[FTP_TRANS_MODE]);
+ if (ftp_parse_eprt_ipport_style(&fctx->cmd_result.arg_refer, &ipd_addr, &port_net) < 0)
+ {
+ return -1;
+ }
+
+ enum session_addr_type saddr_type;
+ struct session_addr *saddr = session_get0_addr(sess, &saddr_type);
+ fctx->last_data_link_key.addr_type = SESSION_ADDR_TYPE_IPV6_TCP;
+ // in active mode, new data link src ip address is server ip address
+ ftp_make_hkey_v6(&fctx->last_data_link_key.tuplev6, (struct in6_addr *)&saddr->ipv6.daddr, &ipd_addr, port_net);
+
+ struct ftp_context *new_data_link_ctx = ftp_decoder_context_deep_dup(fctx);
+ int thread_id = session_get_current_thread_id(sess);
+ ftp_hash_add(fenv->data_link_table[thread_id], &fctx->last_data_link_key.tuplev6, sizeof(struct session_addr_ipv6), new_data_link_ctx);
+
+ ftp_decoder_stat_incrby(thread_id, fenv, FTPD_STAT_NEGOTIATE_DATA_LINK, 1);
+
+ ftp_runtime_log(RLOG_LV_DEBUG, "'%s': EPRT command, port:%u", session_get0_readable_addr(sess), ntohs(port_net));
+ return 0;
+}
+
+/* LPRT support IPv4 and IPv6, pattern: LPRT af,hal,h1,h2,h3,h4...,pal,p1,p2...
+ example:
+ LPRT 6,16,32,2,81,131,67,131,0,0,0,0,0,0,81,131,67,131,2,4,7
+ refer: https://www.rfc-editor.org/rfc/rfc1639.html
+*/
+int ftp_cmd_handler_do_lprt(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv)
+{
+ // todo
+ ftp_runtime_log(RLOG_LV_INFO, "LPRT command not support yet!");
+ return -1;
+}
+
+/* EPSV support IPv4 and IPv6,
+ refer: https://datatracker.ietf.org/doc/html/rfc2428#autoid-3
+*/
+int ftp_cmd_handler_do_epsv(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv)
+{
+ // todo
+ ftp_runtime_log(RLOG_LV_INFO, "EPSV command not support yet!");
+ return -1;
+}
+
+/* LPSV support IPv4 and IPv6, response is 228 xxxx...
+ refer: https://www.rfc-editor.org/rfc/rfc1639.html
+*/
+int ftp_cmd_handler_do_lpsv(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv)
+{
+ // todo
+ ftp_runtime_log(RLOG_LV_INFO, "LPSV command not support yet!");
+ return -1;
+}
+
+static const struct ftp_interact_parser g_ftp_client_cmd_tuple[] =
+ {
+ {FTP_ACCOUNT, "USER", 4, ftp_cmd_handler_do_user},
+ {FTP_PASSWORD, "PASS", 4, ftp_cmd_handler_do_pass},
+ {FTP_TRANS_MODE, "PORT", 4, ftp_cmd_handler_do_port},
+ {FTP_TRANS_MODE, "PASV", 4, ftp_cmd_handler_do_pasv},
+ {FTP_TRANS_MODE, "EPRT", 4, ftp_cmd_handler_do_eprt},
+ {FTP_TRANS_MODE, "EPSV", 4, ftp_cmd_handler_do_epsv},
+ {FTP_TRANS_MODE, "LPRT", 4, ftp_cmd_handler_do_lprt},
+ {FTP_TRANS_MODE, "LPSV", 4, ftp_cmd_handler_do_lpsv},
+ {FTP_MSG_MAX, "LIST", 4, ftp_cmd_handler_do_list},
+ {FTP_TRANS_DIR, "STOR", 4, ftp_cmd_handler_do_stor},
+ {FTP_TRANS_DIR, "RETR", 4, ftp_cmd_handler_do_retr},
+ {FTP_MSG_MAX, NULL, 0, NULL}};
+
+int ftp_res_handler_do_220(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv)
+{
+ fstring_safe_dup(&fctx->cmd_result.arg_refer, &fctx->parse_result.result_array[FTP_BANNER]);
+ return 0;
+}
+
+int ftp_res_handler_do_200(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv)
+{
+ // todo
+ ftp_runtime_log(RLOG_LV_INFO, "'%s': response 200 command not support yet!", session_get0_readable_addr(sess));
+ return -1;
+}
+
+/*
+ example: 227 Entering Passive Mode (218,13,32,6,78,40).
+ */
+int ftp_res_handler_do_227(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv)
+{
+ unsigned int dst_ip_net;
+ unsigned short dst_port_net;
+ if (ftp_parse_ipv4_port_style(&fctx->cmd_result.arg_refer, &dst_ip_net, &dst_port_net) < 0)
+ {
+ return -1;
+ }
+ enum session_addr_type saddr_type;
+ struct session_addr *saddr = session_get0_addr(sess, &saddr_type);
+ fctx->last_data_link_key.addr_type = SESSION_ADDR_TYPE_IPV4_TCP;
+ // in passive mode, new data link src ip address is client ip address
+ ftp_make_hkey_v4(&fctx->last_data_link_key.tuplev4, saddr->ipv4.saddr, dst_ip_net, dst_port_net);
+
+ struct ftp_context *new_data_link_ctx = ftp_decoder_context_deep_dup(fctx);
+ int thread_id = session_get_current_thread_id(sess);
+ ftp_hash_add(fenv->data_link_table[thread_id], &fctx->last_data_link_key.tuplev4, sizeof(struct session_addr_ipv4), new_data_link_ctx);
+ ftp_decoder_stat_incrby(thread_id, fenv, FTPD_STAT_NEGOTIATE_DATA_LINK, 1);
+ ftp_runtime_log(RLOG_LV_DEBUG, "'%s': response 227, %.*s parsed dip:0x%x, dport:%u", session_get0_readable_addr(sess), IOVEC_PRINT(fctx->cmd_result.arg_refer), ntohl(dst_ip_net), ntohs(dst_port_net));
+ return 0;
+}
+
+/*
+ example: 228 Entering Long Passive Mode (af, hal, h1, h2, h3,..., pal, p1, p2...)
+ refer: https://www.rfc-editor.org/rfc/rfc1639.html
+ */
+int ftp_res_handler_do_228(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv)
+{
+ // todo
+ ftp_runtime_log(RLOG_LV_INFO, "'%s': response 228 command not support yet!", session_get0_readable_addr(sess));
+ return -1;
+}
+
+int ftp_parse_ipv6_port_style(const fstring *cmd_str, unsigned short *port_net)
+{
+ unsigned int port_host;
+ char raw_cmd_str_tmp[cmd_str->iov_len + 1] = {};
+ memcpy(raw_cmd_str_tmp, cmd_str->iov_base, cmd_str->iov_len);
+ char only_integer_str_tmp[cmd_str->iov_len + 1] = {};
+ if (sscanf(raw_cmd_str_tmp, "%*[a-zA-Z (|]%[0-9]", only_integer_str_tmp) <= 0)
+ {
+ if (sscanf(raw_cmd_str_tmp, "|||%s|", only_integer_str_tmp) <= 0)
+ {
+ ftp_runtime_log(RLOG_LV_FATAL, "ftp_parse_ipv4_port_style parse error: %.*s", IOVEC_PRINT_PTR(cmd_str));
+ return -1;
+ }
+ }
+ int ret = sscanf(only_integer_str_tmp, "%u", &port_host);
+ if (ret != 1)
+ {
+ ftp_runtime_log(RLOG_LV_FATAL, "ftp_parse_ipv6_port_style parse error: %.*s", cmd_str->iov_len, only_integer_str_tmp);
+ return -1;
+ }
+ *port_net = htons((unsigned short)port_host);
+ return 0;
+}
+/*
+ example: 229 229 Entering Extended Passive Mode (|||20987|)
+ refer: https://datatracker.ietf.org/doc/html/rfc2428#autoid-3
+ */
+int ftp_res_handler_do_229(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv)
+{
+ unsigned short port_net;
+ if (ftp_parse_ipv6_port_style(&fctx->cmd_result.arg_refer, &port_net) < 0)
+ {
+ ftp_runtime_log(RLOG_LV_FATAL, "'%s': response 229 parse error, %.*s", session_get0_readable_addr(sess), IOVEC_PRINT(fctx->cmd_result.arg_refer));
+ return -1;
+ }
+
+ enum session_addr_type saddr_type;
+ struct session_addr *saddr = session_get0_addr(sess, &saddr_type);
+ if (saddr_type != SESSION_ADDR_TYPE_IPV6_TCP)
+ {
+ ftp_runtime_log(RLOG_LV_FATAL, "'%s': response 229, but addr type is not ipv6 %.*s", session_get0_readable_addr(sess), IOVEC_PRINT(fctx->cmd_result.arg_refer));
+ return -1;
+ }
+ fctx->last_data_link_key.addr_type = SESSION_ADDR_TYPE_IPV6_TCP;
+ // in passive mode, new data link src ip address is client ip address
+ ftp_make_hkey_v6(&fctx->last_data_link_key.tuplev6, (struct in6_addr *)saddr->ipv6.saddr, (struct in6_addr *)saddr->ipv6.daddr, port_net);
+
+ struct ftp_context *new_data_link_ctx = ftp_decoder_context_deep_dup(fctx);
+ int thread_id = session_get_current_thread_id(sess);
+ ftp_hash_add(fenv->data_link_table[thread_id], &fctx->last_data_link_key.tuplev6, sizeof(struct session_addr_ipv6), new_data_link_ctx);
+ ftp_decoder_stat_incrby(thread_id, fenv, FTPD_STAT_NEGOTIATE_DATA_LINK, 1);
+ ftp_runtime_log(RLOG_LV_DEBUG, "'%s': EPSV response 229, port is:%u", session_get0_readable_addr(sess), ntohs(port_net));
+ return -1;
+}
+
+static const struct ftp_interact_parser g_ftp_server_response_tuple[] =
+ {
+ {FTP_BANNER, "220", 3, ftp_res_handler_do_220},
+ {FTP_MSG_MAX, "200", 3, ftp_res_handler_do_200},
+ {FTP_MSG_MAX, "227", 3, ftp_res_handler_do_227},
+ {FTP_MSG_MAX, "228", 3, ftp_res_handler_do_228},
+ {FTP_MSG_MAX, "229", 3, ftp_res_handler_do_229},
+ {FTP_MSG_MAX, NULL, 0, NULL}};
+
+int ftp_ctrl_identify_by_payload(const char *payload, size_t len, u_int8_t curdir)
+{
+ if (NULL == payload || len < FTPD_IDENTIRY_MIN_LEN)
+ {
+ return 0;
+ }
+ if (curdir == PACKET_DIRECTION_C2S)
+ {
+ if (memcmp(payload, "USER", 4) == 0)
+ {
+ return 1;
+ }
+ }
+ else
+ {
+ if (memcmp(payload, "220", 3) == 0)
+ {
+ int tmplen = MIN(len, FTPD_IDENTIRY_MAX_LEN);
+ char tmp_buffer[tmplen];
+ memcpy(tmp_buffer, payload, tmplen);
+ ftp_strtolower(tmp_buffer, tmplen);
+ if ((memmem(tmp_buffer, tmplen, "ftp", 3) != NULL))
+ {
+ return 1;
+ }
+ }
+ }
+ return 0;
+}
+
+int ftp_ctrl_identify_by_addr(struct session *sess)
+{
+ enum session_addr_type saddr_type = SESSION_ADDR_TYPE_UNKNOWN;
+ struct session_addr *saddr = session_get0_addr(sess, &saddr_type);
+ unsigned short sport_host = 0, dport_host = 0;
+ if (saddr_type == SESSION_ADDR_TYPE_IPV4_TCP)
+ {
+ sport_host = ntohs(saddr->ipv4.sport);
+ dport_host = ntohs(saddr->ipv4.dport);
+ }
+ else if (saddr_type == SESSION_ADDR_TYPE_IPV6_TCP)
+ {
+ sport_host = ntohs(saddr->ipv6.sport);
+ dport_host = ntohs(saddr->ipv6.dport);
+ }
+ else
+ {
+ return 0;
+ }
+ if (sport_host == 21 || dport_host == 21)
+ {
+ return 1;
+ }
+ if (sport_host == 25 || dport_host == 25)
+ {
+ return 0;
+ }
+ if (sport_host == 110 || dport_host == 110)
+ {
+ return 0;
+ }
+ return 0;
+}
+
+int ftp_ctrl_identify(struct session *sess, const char *payload, size_t len, u_int8_t curdir)
+{
+ if (ftp_ctrl_identify_by_addr(sess))
+ {
+ return 1;
+ }
+ return ftp_ctrl_identify_by_payload(payload, len, curdir);
+}
+
+struct ftp_context *ftp_data_identify(struct session *sess, struct ftp_decoder_env *fenv)
+{
+ struct ftp_link_key hash_key = {};
+ if (ftp_build_hashkey_from_session(sess, &hash_key) < 0)
+ {
+ return NULL;
+ }
+ int thread_id = session_get_current_thread_id(sess);
+ struct ftp_context *declare_fctx = (struct ftp_context *)ftp_hash_search(fenv->data_link_table[thread_id], &hash_key);
+ if (NULL == declare_fctx)
+ {
+ return NULL;
+ }
+ // use tuple3 as hash key, so free it immediately after found!
+ ftp_hash_del(fenv->data_link_table[thread_id], &hash_key);
+ return declare_fctx;
+}
+
+static const struct ftp_interact_parser *ftp_fetch_cmd_parser(const struct ftp_interact_parser *table, const struct ftp_interact_line *cmd_result)
+{
+ for (int i = 0; table[i].cmd_name != NULL; i++)
+ {
+ if (0 == strncmp(table[i].cmd_name, (char *)cmd_result->cmd_refer.iov_base, table[i].cmd_len))
+ {
+ return &table[i];
+ }
+ }
+ return NULL;
+}
+
+int ftp_cmd_process(struct session *sess, struct ftp_context *fctx, struct ftp_decoder_env *fenv, int curdir)
+{
+ const struct ftp_interact_parser *parser;
+ if (PACKET_DIRECTION_C2S == curdir)
+ {
+ parser = ftp_fetch_cmd_parser(g_ftp_client_cmd_tuple, &fctx->cmd_result);
+ }
+ else
+ {
+ parser = ftp_fetch_cmd_parser(g_ftp_server_response_tuple, &fctx->cmd_result);
+ }
+ if (NULL == parser)
+ {
+ ftp_runtime_log(RLOG_LV_INFO, "ftp_fetch_cmd_parser() failed, '%s', not support cmd %.*s", session_get0_readable_addr(sess), IOVEC_PRINT(fctx->cmd_result.cmd_refer));
+ return -1;
+ }
+ if (parser->cmd_handler(sess, fctx, fenv) != -1)
+ {
+ fctx->parse_result.push_result_flags[parser->cmd_msg_type] = 0;
+ ftp_decoder_push_msg(sess, fctx, fenv);
+ ftp_decoder_stat_incrby(session_get_current_thread_id(sess), fenv, FTPD_STAT_CTRL_CMD, 1);
+ }
+ return 0;
+}
diff --git a/src/ftp_decoder_stat.cpp b/src/ftp_decoder_stat.cpp
new file mode 100644
index 0000000..ac5f1ce
--- /dev/null
+++ b/src/ftp_decoder_stat.cpp
@@ -0,0 +1,54 @@
+#include <assert.h>
+#include <fieldstat/fieldstat_easy.h>
+#include "ftp_decoder_stat.h"
+#include "ftp_decoder_inner.h"
+#include "ftp_decoder_util.h"
+
+static const struct ftpd_stat_config_tuple g_ftpd_stat_tuple[] =
+ {
+ {FTPD_STAT_CTRL_LINK_OPEN, "clink_open"},
+ {FTPD_STAT_CTRL_LINK_CLOSE, "clink_close"},
+ {FTPD_STAT_DATA_LINK_OPEN, "dlink_open"},
+ {FTPD_STAT_DATA_LINK_CLOSE, "dlink_close"},
+ {FTPD_STAT_NEGOTIATE_DATA_LINK, "negotiate_dlink"},
+ {FTPD_STAT_CTRL_LINK_BYTES_C2S, "clink_bytes_c2s"},
+ {FTPD_STAT_CTRL_LINK_BYTES_S2C, "clink_bytes_s2c"},
+ {FTPD_STAT_DATA_LINK_BYTES_C2S, "dlink_bytes_c2s"},
+ {FTPD_STAT_DATA_LINK_BYTES_S2C, "dlink_bytes_s2c"},
+ {FTPD_STAT_CTRL_CMD, "ctrl_cmd"},
+};
+
+void ftp_decoder_stat_incrby(int thread_idx, struct ftp_decoder_env *fenv, enum ftp_decoder_stat_type stattype, long long increment)
+{
+ if (fenv->fstat.fs4_instance)
+ {
+ fieldstat_easy_counter_incrby(fenv->fstat.fs4_instance, thread_idx, fenv->fstat.fs4_counter_id[stattype], NULL, 0, increment);
+ }
+}
+
+int ftp_decoder_stat_init(struct ftp_decoder_env *fenv)
+{
+ assert(sizeof(g_ftpd_stat_tuple) / sizeof(struct ftpd_stat_config_tuple) == FTPD_STAT_MAX);
+ fenv->fstat.fs4_instance = fieldstat_easy_new(fenv->thread_count, FTP_DECODER_FIELDSTAT_NAME, NULL, 0);
+
+ for (int i = 0; i < FTPD_STAT_MAX; i++)
+ {
+ fenv->fstat.fs4_counter_id[i] = fieldstat_easy_register_counter(fenv->fstat.fs4_instance, g_ftpd_stat_tuple[i].name);
+ }
+ ftp_mkdir_p("./metrics", 0644);
+ int ret = fieldstat_easy_enable_auto_output(fenv->fstat.fs4_instance, FTP_DECODER_FIELDSTAT_OUTPUT_FILE, FTP_DECODER_FIELDSTAT_OUTPUT_INTERVAL);
+ if (ret < 0)
+ {
+ fprintf(stderr, "fieldstat_easy_enable_auto_output '%s' failed\n", FTP_DECODER_FIELDSTAT_OUTPUT_FILE);
+ fieldstat_easy_free(fenv->fstat.fs4_instance);
+ fenv->fstat.fs4_instance = NULL;
+ return -1;
+ }
+ return 0;
+}
+
+void ftp_decoder_stat_free(struct ftp_decoder_env *fenv)
+{
+ fieldstat_easy_free(fenv->fstat.fs4_instance);
+ fenv->fstat.fs4_instance = NULL;
+} \ No newline at end of file
diff --git a/src/ftp_decoder_stat.h b/src/ftp_decoder_stat.h
new file mode 100644
index 0000000..bfca60c
--- /dev/null
+++ b/src/ftp_decoder_stat.h
@@ -0,0 +1,28 @@
+#pragma once
+#include <fieldstat/fieldstat_easy.h>
+
+enum ftp_decoder_stat_type{
+ FTPD_STAT_CTRL_LINK_OPEN = 0,
+ FTPD_STAT_CTRL_LINK_CLOSE,
+ FTPD_STAT_DATA_LINK_OPEN,
+ FTPD_STAT_DATA_LINK_CLOSE,
+ FTPD_STAT_NEGOTIATE_DATA_LINK, //Negotiating by PORT or PASV command in ctrl link
+ FTPD_STAT_CTRL_LINK_BYTES_C2S,
+ FTPD_STAT_CTRL_LINK_BYTES_S2C,
+ FTPD_STAT_DATA_LINK_BYTES_C2S,
+ FTPD_STAT_DATA_LINK_BYTES_S2C,
+ FTPD_STAT_CTRL_CMD,
+
+ FTPD_STAT_MAX,
+};
+
+
+struct ftpd_stat_config_tuple
+{
+ enum ftp_decoder_stat_type type;
+ const char *name;
+};
+
+int ftp_decoder_stat_init(struct ftp_decoder_env *fenv);
+void ftp_decoder_stat_free(struct ftp_decoder_env *fenv);
+void ftp_decoder_stat_incrby(int thread_idx, struct ftp_decoder_env *fenv, enum ftp_decoder_stat_type stattype, long long increment); \ No newline at end of file
diff --git a/src/ftp_decoder_util.cpp b/src/ftp_decoder_util.cpp
new file mode 100644
index 0000000..8956887
--- /dev/null
+++ b/src/ftp_decoder_util.cpp
@@ -0,0 +1,163 @@
+#include "ftp_decoder_inner.h"
+#include "ftp_decoder.h"
+#include "ftp_decoder_util.h"
+#include <ctype.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <errno.h>
+#include <libgen.h>
+
+void fstring_dup(const fstring *src, fstring *dst)
+{
+ dst->iov_base = (char *)malloc(src->iov_len);
+ assert(dst->iov_base);
+ memcpy((void *)dst->iov_base, src->iov_base, src->iov_len);
+ dst->iov_len = src->iov_len;
+}
+void fstring_safe_dup(const fstring *src, fstring *dst)
+{
+ if (dst->iov_base)
+ {
+ free(dst->iov_base); // free it if exist
+ }
+ fstring_dup(src, dst);
+}
+
+void ftp_parse_result_free(struct ftp_parse_result *parse_result)
+{
+ for (int i = 0; i < FTP_MSG_MAX; i++)
+ {
+ if (i != FTP_INVENTORY && i != FTP_FILE_CONTENT)
+ {
+ if (parse_result->result_array[i].iov_base)
+ {
+ free(parse_result->result_array[i].iov_base);
+ }
+ }
+ }
+}
+
+static void ftp_str_split(const char *payload, size_t len, fstring *left, fstring *rigth, int delim)
+{
+ const char *p = (char *)memchr(payload, delim, len);
+ if (NULL == p)
+ {
+ left->iov_base = (char *)payload;
+ left->iov_len = len;
+ rigth->iov_base = NULL;
+ rigth->iov_len = 0;
+ }
+ else
+ {
+ left->iov_base = (char *)payload;
+ left->iov_len = p - payload;
+ rigth->iov_base = (char *)p + 1;
+ rigth->iov_len = len - left->iov_len - 1;
+ }
+}
+
+long ftp_skip_tail_crlf(const char *payload, long len)
+{
+ long new_len = 0;
+ for (long i = 0; i < len; i++, new_len++)
+ {
+ if ((payload[i] == '\r' || payload[i] == '\n'))
+ {
+ break;
+ }
+ }
+ return new_len;
+}
+
+int ftp_cmd_readline(struct ftp_interact_line *line, const char *payload, size_t len)
+{
+ memset(line, 0, sizeof(struct ftp_interact_line));
+ long skip_crlf_len = ftp_skip_tail_crlf(payload, len);
+ ftp_str_split(payload, skip_crlf_len, &line->cmd_refer, &line->arg_refer, ' ');
+ return 0;
+}
+
+void ftp_strtolower(char *str, size_t len)
+{
+ for (size_t i = 0; i < len; i++)
+ {
+ str[i] = tolower(str[i]);
+ }
+}
+
+int ftp_mkdir_p(const char *path, mode_t mode)
+{
+ struct stat st;
+ errno = 0;
+
+ /* Try to make the directory */
+ if (mkdir(path, mode) == 0)
+ return 0;
+
+ /* If it fails for any reason but EEXIST, fail */
+ if (errno != EEXIST)
+ return -1;
+
+ /* Check if the existing path is a directory */
+ if (stat(path, &st) != 0)
+ return -1;
+
+ /* If not, fail with ENOTDIR */
+ if (!S_ISDIR(st.st_mode))
+ {
+ errno = ENOTDIR;
+ return -1;
+ }
+
+ errno = 0;
+ return 0;
+}
+
+int ftp_session_get_pkt_dir(struct session *sess)
+{
+ const struct packet *pkt = session_get0_current_packet(sess);
+ if (NULL == pkt)
+ {
+ return PACKET_DIRECTION_UNKNOWN;
+ }
+ return packet_get_direction(pkt);
+}
+
+struct ftp_context *ftp_decoder_context_deep_dup(const struct ftp_context *src)
+{
+ struct ftp_context *new_ctx = (struct ftp_context *)calloc(1, sizeof(struct ftp_context));
+ for (int i = 0; i < FTP_MSG_MAX; i++)
+ {
+ if (i != FTP_INVENTORY && i != FTP_FILE_CONTENT)
+ {
+ fstring_safe_dup(&src->parse_result.result_array[i], &new_ctx->parse_result.result_array[i]);
+ }
+ }
+ return new_ctx;
+}
+
+const char *ftp_message_type_to_string(enum ftp_msg_type fmsg_type)
+{
+ switch (fmsg_type)
+ {
+ case FTP_BANNER:
+ return "FTP_BANNER";
+ case FTP_ACCOUNT:
+ return "FTP_ACCOUNT";
+ case FTP_PASSWORD:
+ return "FTP_PASSWORD";
+ case FTP_URI:
+ return "FTP_URI";
+ case FTP_TRANS_MODE:
+ return "FTP_TRANS_MODE";
+ case FTP_TRANS_DIR:
+ return "FTP_TRANS_DIR";
+ case FTP_INVENTORY:
+ return "FTP_INVENTORY";
+ case FTP_FILE_CONTENT:
+ return "FTP_FILE_CONTENT";
+ case FTP_MSG_MAX:
+ return "UNKNOWN";
+ }
+ return "UNKNOWN";
+} \ No newline at end of file
diff --git a/src/ftp_decoder_util.h b/src/ftp_decoder_util.h
new file mode 100644
index 0000000..4e83128
--- /dev/null
+++ b/src/ftp_decoder_util.h
@@ -0,0 +1,25 @@
+#pragma once
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <string.h>
+#include <bits/types/struct_iovec.h>
+
+#ifndef MAX
+#define MAX(a, b) ((a) >= (b) ? (a) : (b))
+#endif
+#ifndef MIN
+#define MIN(a, b) ((a) >= (b) ? (b) : (a))
+#endif
+
+#ifndef fstring
+typedef struct iovec fstring;
+#endif
+
+void fstring_dup(const fstring *src, fstring *dst);
+void fstring_safe_dup(const fstring *src, fstring *dst);
+void ftp_strtolower(char *str, size_t len);
+int ftp_mkdir_p(const char *path, mode_t mode);
+int ftp_session_get_pkt_dir(struct session *sess);
+struct ftp_context *ftp_decoder_context_deep_dup(const struct ftp_context *src);
+long ftp_skip_tail_crlf(const char *payload, long len);
+void ftp_parse_result_free(struct ftp_parse_result *parse_result); \ No newline at end of file
diff --git a/src/version.map b/src/version.map
new file mode 100644
index 0000000..404c631
--- /dev/null
+++ b/src/version.map
@@ -0,0 +1,9 @@
+VERS_3.0{
+global:
+ extern "C" {
+ FTP_ONLOAD;
+ FTP_UNLOAD;
+ ftp_message_*;
+ };
+ local: *;
+};
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
new file mode 100644
index 0000000..6b76516
--- /dev/null
+++ b/test/CMakeLists.txt
@@ -0,0 +1,68 @@
+cmake_minimum_required (VERSION 2.8...3.10)
+set(DECODER_NAME ftp_decoder)
+set(TEST_RUN_DIR ${CMAKE_INSTALL_PREFIX}/sapp)
+set(SAPP_DEVEL_DIR ${TEST_RUN_DIR}/lib)
+set(TEST_MAIN plugin_test_main)
+
+set(TEST_JSON_DIR ${CMAKE_SOURCE_DIR}/test/test_result_json)
+set(TEST_PCAP_DIR ${CMAKE_SOURCE_DIR}/test/ftp_pcap)
+
+add_library(${lib_name}_test_plug SHARED ${lib_name}_test_plug.cpp)
+target_link_libraries(${lib_name}_test_plug MESA_prof_load cjson)
+set_target_properties(${lib_name}_test_plug PROPERTIES PREFIX "")
+
+include_directories(${CMAKE_SOURCE_DIR}/include/)
+include_directories(${CMAKE_SOURCE_DIR}/src/)
+
+file(GLOB DEP_SRC "${CMAKE_SOURCE_DIR}/src/*.cpp")
+
+add_executable(gtest_ftp_decoder gtest_ftp_decoder.cpp ${DEP_SRC})
+target_link_libraries(gtest_ftp_decoder gtest fieldstat4 MESA_handle_logger MESA_prof_load MESA_htable)
+
+#build test env
+add_test(NAME FTP_COPY_SPEC COMMAND sh -c "mkdir -p ${TEST_RUN_DIR}/stellar_plugin/ && cp ${CMAKE_SOURCE_DIR}/bin/ftp_decoder.inf ${TEST_RUN_DIR}/stellar_plugin/spec.toml")
+add_test(NAME FTP_COPY_CONF COMMAND sh -c "mkdir -p ${TEST_RUN_DIR}/conf/ftp_decoder && cp ${CMAKE_SOURCE_DIR}/bin/ftp.conf ${TEST_RUN_DIR}/conf/ftp_decoder/ftp.conf")
+add_test(NAME COPY_PLUGIN_TEST_MAIN COMMAND sh -c "rpm -ql sapp | grep plugin_test_main | xargs -i cp -f {} ${TEST_RUN_DIR}/")
+add_test(NAME UPDATE_SAPP_LOG_LEVEL COMMAND bash -c "sed -i 's/sapp_log.fatal/sapp_log.info/' ${TEST_RUN_DIR}/etc/sapp_log.conf")
+
+# copy plugin lib to be tested
+add_test(NAME FTP_COPY_DECODER_SO COMMAND sh -c "mkdir -p ${TEST_RUN_DIR}/stellar_plugin/${DECODER_NAME} && cp ${CMAKE_BINARY_DIR}/src/${DECODER_NAME}.so ${TEST_RUN_DIR}/stellar_plugin/${DECODER_NAME}/${DECODER_NAME}.so")
+add_test(NAME FTP_COPY_TEST_PLUG_SO COMMAND sh -c "cp ${CMAKE_BINARY_DIR}/test/${DECODER_NAME}_test_plug.so ${TEST_RUN_DIR}/stellar_plugin/${DECODER_NAME}/${DECODER_NAME}_test_plug.so")
+
+set_tests_properties(FTP_COPY_SPEC FTP_COPY_CONF COPY_PLUGIN_TEST_MAIN UPDATE_SAPP_LOG_LEVEL
+ FTP_COPY_DECODER_SO FTP_COPY_TEST_PLUG_SO
+ PROPERTIES FIXTURES_SETUP TestFixture)
+
+# run tests
+add_test(NAME FTP_TEST_V4_PORT COMMAND ./${TEST_MAIN} ${TEST_JSON_DIR}/01-ftp-port-upload-download.json
+ -r ${TEST_PCAP_DIR}/01-ftp-port-upload-download.pcap WORKING_DIRECTORY ${TEST_RUN_DIR})
+add_test(NAME FTP_TEST_V4_PORT_C2S COMMAND ./${TEST_MAIN} ${TEST_JSON_DIR}/01-ftp-port-upload-download_C2S.json
+ -r ${TEST_PCAP_DIR}/01-ftp-port-upload-download_C2S.pcap WORKING_DIRECTORY ${TEST_RUN_DIR})
+add_test(NAME FTP_TEST_V4_PORT_S2C COMMAND ./${TEST_MAIN} ${TEST_JSON_DIR}/01-ftp-port-upload-download_S2C.json
+ -r ${TEST_PCAP_DIR}/01-ftp-port-upload-download_S2C.pcap WORKING_DIRECTORY ${TEST_RUN_DIR})
+add_test(NAME FTP_TEST_V4_NO_BANNER COMMAND ./${TEST_MAIN} ${TEST_JSON_DIR}/04-ftp-banner-no-ftp-characters.json
+ -r ${TEST_PCAP_DIR}/04-ftp-banner-no-ftp-characters.pcap WORKING_DIRECTORY ${TEST_RUN_DIR})
+add_test(NAME FTP_TEST_V4_ONLY_CTRL COMMAND ./${TEST_MAIN} ${TEST_JSON_DIR}/05-only-ctrl-link.json
+ -r ${TEST_PCAP_DIR}/05-only-ctrl-link.pcap WORKING_DIRECTORY ${TEST_RUN_DIR})
+add_test(NAME FTP_TEST_V4_PASV COMMAND ./${TEST_MAIN} ${TEST_JSON_DIR}/06-ftp_pasv-upload-download.json
+ -r ${TEST_PCAP_DIR}/06-ftp_pasv-upload-download.pcap WORKING_DIRECTORY ${TEST_RUN_DIR})
+add_test(NAME FTP_TEST_V4_PASV_C2S COMMAND ./${TEST_MAIN} ${TEST_JSON_DIR}/06-ftp_pasv-upload-download_C2S.json
+ -r ${TEST_PCAP_DIR}/06-ftp_pasv-upload-download_C2S.pcap WORKING_DIRECTORY ${TEST_RUN_DIR})
+add_test(NAME FTP_TEST_V4_PASV_S2C COMMAND ./${TEST_MAIN} ${TEST_JSON_DIR}/06-ftp_pasv-upload-download_S2C.json
+ -r ${TEST_PCAP_DIR}/06-ftp_pasv-upload-download_S2C.pcap WORKING_DIRECTORY ${TEST_RUN_DIR})
+
+add_test(NAME FTP_TEST_V6_1 COMMAND ./${TEST_MAIN} ${TEST_JSON_DIR}/02-ftp_v6_1.json
+ -r ${TEST_PCAP_DIR}/02-ftp_v6_1.pcap WORKING_DIRECTORY ${TEST_RUN_DIR})
+add_test(NAME FTP_TEST_V6_2 COMMAND ./${TEST_MAIN} ${TEST_JSON_DIR}/03-ipv6_eport_upload_download.json
+ -r ${TEST_PCAP_DIR}/03-ipv6_eport_upload_download.pcap WORKING_DIRECTORY ${TEST_RUN_DIR})
+set_tests_properties(FTP_TEST_V4_PORT
+ FTP_TEST_V4_PORT_C2S
+ FTP_TEST_V4_PORT_S2C
+ FTP_TEST_V4_NO_BANNER
+ FTP_TEST_V4_ONLY_CTRL
+ FTP_TEST_V4_PASV
+ FTP_TEST_V4_PASV_C2S
+ FTP_TEST_V4_PASV_S2C
+ FTP_TEST_V6_1
+ FTP_TEST_V6_2
+ PROPERTIES FIXTURES_REQUIRED TestFixture) \ No newline at end of file
diff --git a/test/ftp_decoder_test_plug.cpp b/test/ftp_decoder_test_plug.cpp
new file mode 100644
index 0000000..7e51544
--- /dev/null
+++ b/test/ftp_decoder_test_plug.cpp
@@ -0,0 +1,117 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <assert.h>
+#include "cJSON.h"
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+#include "ftp_decoder.h"
+#include <stellar/stellar.h>
+#include <stellar/session.h>
+#include <stellar/session_mq.h>
+#include <stellar/session_exdata.h>
+extern int commit_test_result_json(cJSON *node, const char *name);
+#ifdef __cplusplus
+}
+#endif
+
+static int g_result_count = 0;
+struct ftp_test_context
+{
+ cJSON *json_root;
+ size_t tot_file_len;
+ int cmd_index;
+};
+
+void ftp_iovec_add_to_json(cJSON *object, const char *key, const struct iovec *fmsg_value, int cmd_index)
+{
+ char *tmp = (char *)calloc(1, fmsg_value->iov_len + 1);
+ memcpy(tmp, fmsg_value->iov_base, fmsg_value->iov_len);
+ char key_with_index[128] = {};
+ snprintf(key_with_index, sizeof(key_with_index), "%s_%d", key, cmd_index);
+ cJSON_AddStringToObject(object, key_with_index, tmp);
+ free(tmp);
+ return ;
+}
+
+extern "C" void *ftp_test_plug_session_ctx_new_cb(struct session *sess, void *plugin_env)
+{
+ struct ftp_test_context *fctx = (struct ftp_test_context *)calloc(1, sizeof(struct ftp_test_context));
+ fctx->json_root = cJSON_CreateObject();
+ return fctx;
+}
+
+extern "C" void ftp_test_session_ctx_free_cb(struct session *sess, void *session_ctx, void *plugin_env)
+{
+ struct ftp_test_context *fctx = (struct ftp_test_context *)session_ctx;
+ if(fctx->tot_file_len > 0){
+ cJSON_AddNumberToObject(fctx->json_root, "total-payload-len", fctx->tot_file_len);
+ }
+ char result_name[16]="";
+ sprintf(result_name,"FTP_RESULT_%d", g_result_count);
+ commit_test_result_json(fctx->json_root, result_name);
+ g_result_count+=1;
+ free(session_ctx);
+}
+
+extern "C" void FTP_TEST_PLUG_DATA_ENTRY(struct session *sess, int topic_id, const void *msg, void *per_session_ctx, void *plugin_env)
+{
+ struct ftp_test_context *fctx = (struct ftp_test_context *)per_session_ctx;
+ struct ftp_message *fmsg = (struct ftp_message *)msg;
+ enum session_state sstate = session_get_current_state(sess);
+ if(SESSION_STATE_OPENING == sstate){
+ cJSON_AddStringToObject(fctx->json_root, "FTP_LINK_TYPE", "DATA");
+ cJSON_AddStringToObject(fctx->json_root, "TUPLE4", session_get0_readable_addr(sess));
+ }
+ enum ftp_msg_type fmsg_type;
+ struct iovec fmsg_value;
+ while(ftp_message_iterate(fmsg, &fmsg_type, &fmsg_value)){
+ if(fmsg_type == FTP_INVENTORY || fmsg_type == FTP_FILE_CONTENT){
+ fctx->tot_file_len += fmsg_value.iov_len;
+ }else{
+ // printf("### ftp data plug: len=%d, %.*s\n", (int)fmsg_value.iov_len, (int)fmsg_value.iov_len, (char *)fmsg_value.iov_base);
+ ftp_iovec_add_to_json(fctx->json_root, ftp_message_type_to_string(fmsg_type), &fmsg_value, ++fctx->cmd_index);
+ }
+ }
+}
+extern "C" void FTP_TEST_PLUG_CTRL_ENTRY(struct session *sess, int topic_id, const void *msg, void *per_session_ctx, void *plugin_env)
+{
+ struct ftp_test_context *fctx = (struct ftp_test_context *)per_session_ctx;
+ struct ftp_message *fmsg = (struct ftp_message *)msg;
+ enum session_state sstate = session_get_current_state(sess);
+ if(SESSION_STATE_OPENING == sstate){
+ cJSON_AddStringToObject(fctx->json_root, "FTP_LINK_TYPE", "CTRL");
+ cJSON_AddStringToObject(fctx->json_root, "TUPLE4", session_get0_readable_addr(sess));
+ }
+ enum ftp_msg_type fmsg_type;
+ struct iovec fmsg_value;
+ while(ftp_message_iterate(fmsg, &fmsg_type, &fmsg_value)){
+ // printf("### ftp ctrl plug: len=%d, %.*s\n", (int)fmsg_value.iov_len, (int)fmsg_value.iov_len, (char *)fmsg_value.iov_base);
+ ftp_iovec_add_to_json(fctx->json_root, ftp_message_type_to_string(fmsg_type), &fmsg_value, ++fctx->cmd_index);
+ }
+}
+
+extern "C" void *FTP_TEST_PLUG_INIT(struct stellar *st)
+{
+ void *fake_ftp_test_plugin_env = (void *)"_fake_plugin_env_";
+ int ftp_test_plug_id = stellar_session_plugin_register(st, ftp_test_plug_session_ctx_new_cb, ftp_test_session_ctx_free_cb, fake_ftp_test_plugin_env);
+ int ftp_ctrl_topic_id = stellar_session_mq_get_topic_id(st, FTP_DECODER_CTRL_TOPIC);
+ assert(ftp_ctrl_topic_id >= 0);
+ int ftp_data_topic_id = stellar_session_mq_get_topic_id(st, FTP_DECODER_DATA_TOPIC);
+ assert(ftp_data_topic_id >= 0);
+
+ stellar_session_mq_subscribe(st, ftp_ctrl_topic_id, FTP_TEST_PLUG_CTRL_ENTRY, ftp_test_plug_id);
+ stellar_session_mq_subscribe(st, ftp_data_topic_id, FTP_TEST_PLUG_DATA_ENTRY, ftp_test_plug_id);
+
+ return fake_ftp_test_plugin_env;
+}
+
+extern "C" void FTP_TEST_PLUG_DESTROY(void *plugin_env)
+{
+ //do nothing
+ return ;
+} \ No newline at end of file
diff --git a/test/ftp_pcap/01-ftp-port-upload-download.pcap b/test/ftp_pcap/01-ftp-port-upload-download.pcap
new file mode 100644
index 0000000..314fa6f
--- /dev/null
+++ b/test/ftp_pcap/01-ftp-port-upload-download.pcap
Binary files differ
diff --git a/test/ftp_pcap/01-ftp-port-upload-download_C2S.pcap b/test/ftp_pcap/01-ftp-port-upload-download_C2S.pcap
new file mode 100644
index 0000000..d8f9b06
--- /dev/null
+++ b/test/ftp_pcap/01-ftp-port-upload-download_C2S.pcap
Binary files differ
diff --git a/test/ftp_pcap/01-ftp-port-upload-download_S2C.pcap b/test/ftp_pcap/01-ftp-port-upload-download_S2C.pcap
new file mode 100644
index 0000000..c0e9cc7
--- /dev/null
+++ b/test/ftp_pcap/01-ftp-port-upload-download_S2C.pcap
Binary files differ
diff --git a/test/ftp_pcap/02-ftp_v6_1.pcap b/test/ftp_pcap/02-ftp_v6_1.pcap
new file mode 100644
index 0000000..9b7a924
--- /dev/null
+++ b/test/ftp_pcap/02-ftp_v6_1.pcap
Binary files differ
diff --git a/test/ftp_pcap/03-ipv6_eport_upload_download.pcap b/test/ftp_pcap/03-ipv6_eport_upload_download.pcap
new file mode 100644
index 0000000..167507a
--- /dev/null
+++ b/test/ftp_pcap/03-ipv6_eport_upload_download.pcap
Binary files differ
diff --git a/test/ftp_pcap/04-ftp-banner-no-ftp-characters.pcap b/test/ftp_pcap/04-ftp-banner-no-ftp-characters.pcap
new file mode 100644
index 0000000..f646ff2
--- /dev/null
+++ b/test/ftp_pcap/04-ftp-banner-no-ftp-characters.pcap
Binary files differ
diff --git a/test/ftp_pcap/05-only-ctrl-link.pcap b/test/ftp_pcap/05-only-ctrl-link.pcap
new file mode 100644
index 0000000..8a5da68
--- /dev/null
+++ b/test/ftp_pcap/05-only-ctrl-link.pcap
Binary files differ
diff --git a/test/ftp_pcap/06-ftp_pasv-upload-download.pcap b/test/ftp_pcap/06-ftp_pasv-upload-download.pcap
new file mode 100644
index 0000000..1a391ae
--- /dev/null
+++ b/test/ftp_pcap/06-ftp_pasv-upload-download.pcap
Binary files differ
diff --git a/test/ftp_pcap/06-ftp_pasv-upload-download_C2S.pcap b/test/ftp_pcap/06-ftp_pasv-upload-download_C2S.pcap
new file mode 100644
index 0000000..753f328
--- /dev/null
+++ b/test/ftp_pcap/06-ftp_pasv-upload-download_C2S.pcap
Binary files differ
diff --git a/test/ftp_pcap/06-ftp_pasv-upload-download_S2C.pcap b/test/ftp_pcap/06-ftp_pasv-upload-download_S2C.pcap
new file mode 100644
index 0000000..5ccd508
--- /dev/null
+++ b/test/ftp_pcap/06-ftp_pasv-upload-download_S2C.pcap
Binary files differ
diff --git a/test/ftp_pcap/10-ftp-sdedu.pcap b/test/ftp_pcap/10-ftp-sdedu.pcap
new file mode 100644
index 0000000..cf53ce9
--- /dev/null
+++ b/test/ftp_pcap/10-ftp-sdedu.pcap
Binary files differ
diff --git a/test/gtest_ftp_decoder.cpp b/test/gtest_ftp_decoder.cpp
new file mode 100644
index 0000000..4ea20e3
--- /dev/null
+++ b/test/gtest_ftp_decoder.cpp
@@ -0,0 +1,251 @@
+#include <gtest/gtest.h>
+#include <unistd.h>
+#include <getopt.h>
+#include <arpa/inet.h>
+#include "ftp_decoder.h"
+#include "ftp_decoder_inner.h"
+#include "ftp_decoder_util.h"
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+#include <stellar/session.h>
+#include <stellar/session_exdata.h>
+#include <stellar/session_mq.h>
+#include <stellar/stellar.h>
+#ifdef __cplusplus
+}
+#endif
+/********************* Stellar Mock ********************/
+int stellar_get_worker_thread_num(struct stellar *st) { return 1; }
+int stellar_get_current_thread_id(struct stellar *st) { return 0; }
+int session_get_current_thread_id(struct session *sess) { return 0; }
+const char *session_get0_readable_addr(struct session *sess) { return "TODO"; }
+struct session_addr *session_get0_addr(struct session *sess, enum session_addr_type *addr_type)
+{
+ *addr_type = SESSION_ADDR_TYPE_UNKNOWN;
+ return NULL;
+}
+int session_mq_publish_message(struct session *sess, int topic_id, void *msg) { return 0; }
+const char *session_get0_current_payload(struct session *sess, size_t *payload_len)
+{
+ *payload_len = 0;
+ return "TODO";
+}
+void stellar_session_plugin_dettach_current_session(struct session *sess) { return; }
+enum session_state session_get_current_state(struct session *sess) { return SESSION_STATE_INVALID; }
+int stellar_session_mq_create_topic(struct stellar *st, const char *topic_name, session_msg_free_cb_func *msg_free_cb, void *msg_free_arg) { return 0; }
+int stellar_session_mq_subscribe(struct stellar *st, int topic_id, on_session_msg_cb_func *plugin_on_msg_cb, int plugin_id) { return 0; }
+int stellar_session_plugin_register(struct stellar *st, session_ctx_new_func session_ctx_new, session_ctx_free_func session_ctx_free, void *plugin_env) { return 0; }
+int stellar_session_mq_get_topic_id(struct stellar *st, const char *topic_name) { return 0; }
+const struct packet *session_get0_current_packet(struct session *sess) { return NULL; }
+int packet_get_direction(const struct packet *pkt) { return PACKET_DIRECTION_UNKNOWN; }
+/********************* Stellar Mock End ********************/
+
+TEST(ftp_decoder, identify_by_payload)
+{
+ // true
+ ASSERT_TRUE(ftp_ctrl_identify_by_payload("USER", 4, PACKET_DIRECTION_C2S));
+ ASSERT_TRUE(ftp_ctrl_identify_by_payload("USER test", 9, PACKET_DIRECTION_C2S));
+ ASSERT_TRUE(ftp_ctrl_identify_by_payload("220 (vsFTPd 3.0.1)", strlen("220 (vsFTPd 3.0.1)"), PACKET_DIRECTION_S2C));
+ ASSERT_TRUE(ftp_ctrl_identify_by_payload("220 (vsFTPD 3.0.2)", strlen("220 (vsFTPD 3.0.2)"), PACKET_DIRECTION_S2C));
+ ASSERT_TRUE(ftp_ctrl_identify_by_payload("220 (ftpd 3.0.3)", strlen("220 (ftpd 3.0.3)"), PACKET_DIRECTION_S2C));
+ ASSERT_TRUE(ftp_ctrl_identify_by_payload("220-Microsoft FTP Service", strlen("220-Microsoft FTP Service"), PACKET_DIRECTION_S2C));
+
+ // false
+ ASSERT_FALSE(ftp_ctrl_identify_by_payload("USR", 3, PACKET_DIRECTION_C2S));
+ ASSERT_FALSE(ftp_ctrl_identify_by_payload("USRxx", 5, PACKET_DIRECTION_C2S));
+ ASSERT_FALSE(ftp_ctrl_identify_by_payload("PASS", 4, PACKET_DIRECTION_C2S));
+ ASSERT_FALSE(ftp_ctrl_identify_by_payload("200 OK", 6, PACKET_DIRECTION_S2C));
+ ASSERT_FALSE(ftp_ctrl_identify_by_payload("220 (xxx 3.0.3)", strlen("220 (xxx 3.0.3)"), PACKET_DIRECTION_S2C));
+}
+
+TEST(ftp_decoder, ftp_cmd_readline)
+{
+ struct ftp_interact_line line;
+ ftp_cmd_readline(&line, "USER test", strlen("USER test"));
+ ASSERT_EQ(line.cmd_refer.iov_len, 4);
+ ASSERT_EQ(line.arg_refer.iov_len, 4);
+ ASSERT_EQ(0, strncmp("USER", (char *)line.cmd_refer.iov_base, 4));
+ ASSERT_EQ(0, strncmp("test", (char *)line.arg_refer.iov_base, 4));
+
+ ftp_cmd_readline(&line, "CUSTOM xx1 xx2 xx3", strlen("CUSTOM xx1 xx2 xx3"));
+ ASSERT_EQ(line.cmd_refer.iov_len, 6);
+ ASSERT_EQ(line.arg_refer.iov_len, 11);
+ ASSERT_EQ(0, strncmp("CUSTOM", (char *)line.cmd_refer.iov_base, 6));
+ ASSERT_EQ(0, strncmp("xx1 xx2 xx3", (char *)line.arg_refer.iov_base, 11));
+}
+
+TEST(ftp_decoder, ftp_skip_tail_crlf)
+{
+ ASSERT_EQ(ftp_skip_tail_crlf("", 0), 0);
+ ASSERT_EQ(ftp_skip_tail_crlf("\r", 1), 0);
+ ASSERT_EQ(ftp_skip_tail_crlf("\n", 1), 0);
+ ASSERT_EQ(ftp_skip_tail_crlf("\r\n", 2), 0);
+ ASSERT_EQ(ftp_skip_tail_crlf("a\r\n", 3), 1);
+ ASSERT_EQ(ftp_skip_tail_crlf("abcdefg\r\n", 9), 7);
+}
+
+TEST(ftp_decoder, ipv4_port_style)
+{
+ int ret;
+ uint32_t ipv4_net;
+ uint16_t port_net;
+ fstring ip_port_style;
+ // normal
+ ip_port_style.iov_base = (void *)"192,168,38,2,202,95";
+ ip_port_style.iov_len = strlen("192,168,38,2,202,95");
+ ret = ftp_parse_ipv4_port_style(&ip_port_style, &ipv4_net, &port_net);
+ ASSERT_EQ(ret, 0);
+ ASSERT_EQ(ipv4_net, htonl(0xC0A82602));
+ ASSERT_EQ(port_net, htons(51807));
+
+ // add tab and blank
+ ip_port_style.iov_base = (void *)" 192,168,38,2,202,95 ";
+ ip_port_style.iov_len = strlen(" 192,168,38,2,202,95 ");
+ ret = ftp_parse_ipv4_port_style(&ip_port_style, &ipv4_net, &port_net);
+ ASSERT_EQ(ret, 0);
+ ASSERT_EQ(ipv4_net, htonl(0xC0A82602));
+ ASSERT_EQ(port_net, htons(51807));
+
+ // add bracket
+ ip_port_style.iov_base = (void *)"(192,168,38,2,202,95)";
+ ip_port_style.iov_len = strlen("(192,168,38,2,202,95)");
+ ret = ftp_parse_ipv4_port_style(&ip_port_style, &ipv4_net, &port_net);
+ ASSERT_EQ(ret, 0);
+ ASSERT_EQ(ipv4_net, htonl(0xC0A82602));
+ ASSERT_EQ(port_net, htons(51807));
+
+ // PASV response
+ ip_port_style.iov_base = (void *)"Entering Passive Mode(192,168,38,2,202,95)";
+ ip_port_style.iov_len = strlen("Entering Passive Mode(192,168,38,2,202,95)");
+ ret = ftp_parse_ipv4_port_style(&ip_port_style, &ipv4_net, &port_net);
+ ASSERT_EQ(ret, 0);
+ ASSERT_EQ(ipv4_net, htonl(0xC0A82602));
+ ASSERT_EQ(port_net, htons(51807));
+
+ // PASV response with blank
+ ip_port_style.iov_base = (void *)"Entering Passive Mode ( 192,168,38,2,202,95 )";
+ ip_port_style.iov_len = strlen("Entering Passive Mode ( 192,168,38,2,202,95 )");
+ ret = ftp_parse_ipv4_port_style(&ip_port_style, &ipv4_net, &port_net);
+ ASSERT_EQ(ret, 0);
+ ASSERT_EQ(ipv4_net, htonl(0xC0A82602));
+ ASSERT_EQ(port_net, htons(51807));
+}
+
+TEST(ftp_decoder, ipv6_port_style)
+{
+ int ret;
+ uint16_t port_net;
+ fstring ip_port_style;
+ // normal
+ ip_port_style.iov_base = (void *)"Entering Extended Passive Mode (|||12345|)";
+ ip_port_style.iov_len = strlen("Entering Extended Passive Mode (|||12345|)");
+ ret = ftp_parse_ipv6_port_style(&ip_port_style, &port_net);
+ ASSERT_EQ(ret, 0);
+ ASSERT_EQ(port_net, htons(12345));
+
+ // add blank
+ ip_port_style.iov_base = (void *)"Entering Extended Passive Mode ( |||12345| )";
+ ip_port_style.iov_len = strlen("Entering Extended Passive Mode ( |||12345| )");
+ ret = ftp_parse_ipv6_port_style(&ip_port_style, &port_net);
+ ASSERT_EQ(ret, 0);
+ ASSERT_EQ(port_net, htons(12345));
+}
+
+TEST(ftp_decoder, ipv6_eprt_style)
+{
+ int ret;
+ uint16_t port_net = 0;
+ fstring eprt_style;
+ struct in6_addr ipv6addr = {};
+ // normal
+ eprt_style.iov_base = (void *)"|2|1234::abcd|12345|";
+ eprt_style.iov_len = strlen("|2|1234::abcd|12345|");
+ ret = ftp_parse_eprt_ipport_style(&eprt_style, &ipv6addr, &port_net);
+ ASSERT_EQ(ret, 0);
+ struct in6_addr tmpaddr;
+ inet_pton(AF_INET6, "1234::abcd", &tmpaddr);
+ ASSERT_EQ(0, memcmp(&tmpaddr, &ipv6addr, sizeof(struct in6_addr)));
+ ASSERT_EQ(port_net, htons(12345));
+
+ // loopback address
+ eprt_style.iov_base = (void *)"|2|::1|12345|";
+ eprt_style.iov_len = strlen("|2|::1|12345|");
+ ret = ftp_parse_eprt_ipport_style(&eprt_style, &ipv6addr, &port_net);
+ ASSERT_EQ(ret, 0);
+ inet_pton(AF_INET6, "::1", &tmpaddr);
+ ASSERT_EQ(0, memcmp(&tmpaddr, &ipv6addr, sizeof(struct in6_addr)));
+ ASSERT_EQ(port_net, htons(12345));
+
+ // add blank
+ eprt_style.iov_base = (void *)" |2|1234:4321::abcd|12345| ";
+ eprt_style.iov_len = strlen(" |2|1234:4321::abcd|12345| ");
+ ret = ftp_parse_eprt_ipport_style(&eprt_style, &ipv6addr, &port_net);
+ ASSERT_EQ(ret, 0);
+ inet_pton(AF_INET6, "1234:4321::abcd", &tmpaddr);
+ ASSERT_EQ(0, memcmp(&tmpaddr, &ipv6addr, sizeof(struct in6_addr)));
+ ASSERT_EQ(port_net, htons(12345));
+}
+
+static const char *gtest_cla_short_options = "hLf:";
+static const struct option gtest_cla_long_options[] =
+ {
+ {"help", no_argument, NULL, 'h'},
+ {"gtest_list_tests", no_argument, NULL, 'L'},
+ {"gtest_filter", required_argument, NULL, 'f'},
+ {NULL, 0, NULL, 0}};
+
+static void usage(int argc, char *argv[])
+{
+ printf("args example:\n");
+ printf("\t%s short-arg long-arg\n", argv[0]);
+ printf("\t%s -v \t--version\n", argv[0]);
+ printf("\t%s -L \t--gtest_list_tests\n", argv[0]);
+ printf("\t%s -f \t--gtest_filter=ftp_decoder*\n", argv[0]);
+ exit(0);
+}
+
+int main(int argc, char **argv)
+{
+ int c;
+ int to_gtest_argc = 1;
+ char *to_gtest_args[4] = {(char *)"ftp_gtest", NULL, NULL, NULL};
+ char temp_string[1024] = {};
+ while (1)
+ {
+ c = getopt_long(argc, argv, gtest_cla_short_options, gtest_cla_long_options, NULL);
+ if (c == -1)
+ {
+ break;
+ }
+
+ switch (c)
+ {
+
+ case 'h':
+ usage(argc, argv);
+ break;
+
+ case 'L':
+ to_gtest_args[1] = (char *)"--gtest_list_tests";
+ to_gtest_argc++;
+ break;
+
+ case 'f':
+ strncpy(temp_string, "--gtest_filter=", strlen("--gtest_filter=") + 1);
+ strcat(temp_string, optarg);
+ to_gtest_argc++;
+ to_gtest_args[1] = temp_string;
+ break;
+
+ case '?': /* invalid or unknown option */
+ return -1;
+ break;
+ }
+ }
+
+ testing::InitGoogleTest(&to_gtest_argc, to_gtest_args);
+ int ret = RUN_ALL_TESTS();
+ return ret;
+}
diff --git a/test/test_result_json/01-ftp-port-upload-download.json b/test/test_result_json/01-ftp-port-upload-download.json
new file mode 100644
index 0000000..522beb5
--- /dev/null
+++ b/test/test_result_json/01-ftp-port-upload-download.json
@@ -0,0 +1,66 @@
+[
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "192.168.40.139.20>192.168.38.2.51808",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_URI_4": "ftp://192.168.40.139/2001.03147v1.pdf",
+ "FTP_TRANS_MODE_5": "PORT",
+ "FTP_TRANS_DIR_6": "STOR",
+ "total-payload-len": 681116,
+ "name": "FTP_RESULT_0"
+ },
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "192.168.40.139.20>192.168.38.2.51809",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_TRANS_MODE_4": "PORT",
+ "FTP_TRANS_DIR_5": "LIST",
+ "total-payload-len": 76,
+ "name": "FTP_RESULT_1"
+ },
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "192.168.40.139.20>192.168.38.2.51810",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_URI_4": "ftp://192.168.40.139/3cdaemon-v2r10.zip",
+ "FTP_TRANS_MODE_5": "PORT",
+ "FTP_TRANS_DIR_6": "RETR",
+ "total-payload-len": 953457,
+ "name": "FTP_RESULT_2"
+ },
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "192.168.40.139.20>192.168.38.2.51811",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_TRANS_MODE_4": "PORT",
+ "FTP_TRANS_DIR_5": "LIST",
+ "total-payload-len": 76,
+ "name": "FTP_RESULT_3"
+ },
+ {
+ "FTP_LINK_TYPE": "CTRL",
+ "TUPLE4": "192.168.38.2.51805>192.168.40.139.21",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_TRANS_MODE_4": "PORT",
+ "FTP_TRANS_DIR_5": "LIST",
+ "FTP_TRANS_MODE_6": "PORT",
+ "FTP_URI_7": "ftp://192.168.40.139/2001.03147v1.pdf",
+ "FTP_TRANS_DIR_8": "STOR",
+ "FTP_TRANS_MODE_9": "PORT",
+ "FTP_TRANS_MODE_10": "PORT",
+ "FTP_URI_11": "ftp://192.168.40.139/3cdaemon-v2r10.zip",
+ "FTP_TRANS_DIR_12": "RETR",
+ "FTP_TRANS_MODE_13": "PORT",
+ "name": "FTP_RESULT_4"
+ }
+] \ No newline at end of file
diff --git a/test/test_result_json/01-ftp-port-upload-download_C2S.json b/test/test_result_json/01-ftp-port-upload-download_C2S.json
new file mode 100644
index 0000000..d04e6a9
--- /dev/null
+++ b/test/test_result_json/01-ftp-port-upload-download_C2S.json
@@ -0,0 +1,28 @@
+[
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "192.168.40.139.20>192.168.38.2.51808",
+ "FTP_ACCOUNT_1": "ftp",
+ "FTP_PASSWORD_2": "111111",
+ "FTP_URI_3": "ftp://192.168.40.139/2001.03147v1.pdf",
+ "FTP_TRANS_MODE_4": "PORT",
+ "FTP_TRANS_DIR_5": "STOR",
+ "total-payload-len": 681116,
+ "name": "FTP_RESULT_0"
+ },
+ {
+ "FTP_ACCOUNT_1": "ftp",
+ "FTP_PASSWORD_2": "111111",
+ "FTP_TRANS_MODE_3": "PORT",
+ "FTP_TRANS_DIR_4": "LIST",
+ "FTP_TRANS_MODE_5": "PORT",
+ "FTP_URI_6": "ftp://192.168.40.139/2001.03147v1.pdf",
+ "FTP_TRANS_DIR_7": "STOR",
+ "FTP_TRANS_MODE_8": "PORT",
+ "FTP_TRANS_MODE_9": "PORT",
+ "FTP_URI_10": "ftp://192.168.40.139/3cdaemon-v2r10.zip",
+ "FTP_TRANS_DIR_11": "RETR",
+ "FTP_TRANS_MODE_12": "PORT",
+ "name": "FTP_RESULT_1"
+ }
+] \ No newline at end of file
diff --git a/test/test_result_json/01-ftp-port-upload-download_S2C.json b/test/test_result_json/01-ftp-port-upload-download_S2C.json
new file mode 100644
index 0000000..346c6dc
--- /dev/null
+++ b/test/test_result_json/01-ftp-port-upload-download_S2C.json
@@ -0,0 +1,8 @@
+[
+ {
+ "FTP_LINK_TYPE": "CTRL",
+ "TUPLE4": "192.168.38.2.51805>192.168.40.139.21",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "name": "FTP_RESULT_0"
+ }
+] \ No newline at end of file
diff --git a/test/test_result_json/02-ftp_v6_1.json b/test/test_result_json/02-ftp_v6_1.json
new file mode 100644
index 0000000..1a6e236
--- /dev/null
+++ b/test/test_result_json/02-ftp_v6_1.json
@@ -0,0 +1,57 @@
+[
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "::1.39002>::1.10791",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_TRANS_DIR_4": "LIST",
+ "total-payload-len": 65,
+ "name": "FTP_RESULT_0"
+ },
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "::1.41426>::1.20987",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_URI_4": "ftp://::1/123.txt",
+ "FTP_TRANS_DIR_5": "RETR",
+ "total-payload-len": 44,
+ "name": "FTP_RESULT_1"
+ },
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "::1.59846>::1.50928",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_URI_4": "ftp://::1/test.txt",
+ "FTP_TRANS_DIR_5": "STOR",
+ "total-payload-len": 140,
+ "name": "FTP_RESULT_2"
+ },
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "::1.50872>::1.14779",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_TRANS_DIR_4": "LIST",
+ "total-payload-len": 131,
+ "name": "FTP_RESULT_3"
+ },
+ {
+ "FTP_LINK_TYPE": "CTRL",
+ "TUPLE4": "::1.47756>::1.21",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_TRANS_DIR_4": "LIST",
+ "FTP_URI_5": "ftp://::1/123.txt",
+ "FTP_TRANS_DIR_6": "RETR",
+ "FTP_URI_7": "ftp://::1/test.txt",
+ "FTP_TRANS_DIR_8": "STOR",
+ "name": "FTP_RESULT_4"
+ }
+] \ No newline at end of file
diff --git a/test/test_result_json/03-ipv6_eport_upload_download.json b/test/test_result_json/03-ipv6_eport_upload_download.json
new file mode 100644
index 0000000..557b942
--- /dev/null
+++ b/test/test_result_json/03-ipv6_eport_upload_download.json
@@ -0,0 +1,77 @@
+[
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "::1.20>::1.49998",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_TRANS_MODE_4": "EPRT",
+ "FTP_TRANS_DIR_5": "LIST",
+ "total-payload-len": 65,
+ "name": "FTP_RESULT_0"
+ },
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "::1.20>::1.40784",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_URI_4": "ftp://::1/123.txt",
+ "FTP_TRANS_MODE_5": "EPRT",
+ "FTP_TRANS_DIR_6": "RETR",
+ "total-payload-len": 44,
+ "name": "FTP_RESULT_1"
+ },
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "::1.20>::1.32865",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_TRANS_MODE_4": "EPRT",
+ "FTP_TRANS_DIR_5": "LIST",
+ "total-payload-len": 65,
+ "name": "FTP_RESULT_2"
+ },
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "::1.20>::1.55267",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_URI_4": "ftp://::1/test.txt",
+ "FTP_TRANS_MODE_5": "EPRT",
+ "FTP_TRANS_DIR_6": "STOR",
+ "total-payload-len": 140,
+ "name": "FTP_RESULT_3"
+ },
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "::1.20>::1.34844",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_TRANS_MODE_4": "EPRT",
+ "FTP_TRANS_DIR_5": "LIST",
+ "total-payload-len": 131,
+ "name": "FTP_RESULT_4"
+ },
+ {
+ "FTP_LINK_TYPE": "CTRL",
+ "TUPLE4": "::1.49594>::1.21",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_TRANS_MODE_4": "EPRT",
+ "FTP_TRANS_DIR_5": "LIST",
+ "FTP_TRANS_MODE_6": "EPRT",
+ "FTP_URI_7": "ftp://::1/123.txt",
+ "FTP_TRANS_DIR_8": "RETR",
+ "FTP_TRANS_MODE_9": "EPRT",
+ "FTP_TRANS_MODE_10": "EPRT",
+ "FTP_URI_11": "ftp://::1/test.txt",
+ "FTP_TRANS_DIR_12": "STOR",
+ "FTP_TRANS_MODE_13": "EPRT",
+ "name": "FTP_RESULT_5"
+ }
+] \ No newline at end of file
diff --git a/test/test_result_json/04-ftp-banner-no-ftp-characters.json b/test/test_result_json/04-ftp-banner-no-ftp-characters.json
new file mode 100644
index 0000000..ddb097b
--- /dev/null
+++ b/test/test_result_json/04-ftp-banner-no-ftp-characters.json
@@ -0,0 +1,26 @@
+[
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "10.20.144.150.35976>10.20.144.151.16013",
+ "FTP_BANNER_1": "Connection will close if idle more than 5 minutes.",
+ "FTP_ACCOUNT_2": "cdts3500",
+ "FTP_PASSWORD_3": "cdts3500",
+ "FTP_URI_4": "ftp://10.20.144.151/qgpl/apkeyf.apkeyf",
+ "FTP_TRANS_MODE_5": "PASV",
+ "FTP_TRANS_DIR_6": "RETR",
+ "total-payload-len": 439,
+ "name": "FTP_RESULT_0"
+ },
+ {
+ "FTP_LINK_TYPE": "CTRL",
+ "TUPLE4": "10.20.144.150.35974>10.20.144.151.21",
+ "FTP_BANNER_1": "at fran.csg.stercomm.com.",
+ "FTP_BANNER_2": "Connection will close if idle more than 5 minutes.",
+ "FTP_ACCOUNT_3": "cdts3500",
+ "FTP_PASSWORD_4": "cdts3500",
+ "FTP_TRANS_MODE_5": "PASV",
+ "FTP_URI_6": "ftp://10.20.144.151/qgpl/apkeyf.apkeyf",
+ "FTP_TRANS_DIR_7": "RETR",
+ "name": "FTP_RESULT_1"
+ }
+] \ No newline at end of file
diff --git a/test/test_result_json/05-only-ctrl-link.json b/test/test_result_json/05-only-ctrl-link.json
new file mode 100644
index 0000000..a4a5068
--- /dev/null
+++ b/test/test_result_json/05-only-ctrl-link.json
@@ -0,0 +1,11 @@
+[{
+ "FTP_LINK_TYPE": "CTRL",
+ "TUPLE4": "192.168.20.1.40920>192.168.60.1.21",
+ "FTP_BANNER_1": "(vsFTPd 3.0.2)",
+ "FTP_ACCOUNT_2": "test1",
+ "FTP_PASSWORD_3": "iiecas2021",
+ "FTP_TRANS_MODE_4": "PASV",
+ "FTP_URI_5": "ftp://192.168.60.1//home/test1/20210622201359人肉炸弹.txt",
+ "FTP_TRANS_DIR_6": "STOR",
+ "name": "FTP_RESULT_0"
+}] \ No newline at end of file
diff --git a/test/test_result_json/06-ftp_pasv-upload-download.json b/test/test_result_json/06-ftp_pasv-upload-download.json
new file mode 100644
index 0000000..7ba734d
--- /dev/null
+++ b/test/test_result_json/06-ftp_pasv-upload-download.json
@@ -0,0 +1,65 @@
+[
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "127.0.0.1.59814>127.0.0.1.13144",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_TRANS_MODE_4": "PASV",
+ "FTP_TRANS_DIR_5": "LIST",
+ "total-payload-len": 61,
+ "name": "FTP_RESULT_0"
+ },
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "127.0.0.1.38754>127.0.0.1.15810",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_TRANS_MODE_4": "PASV",
+ "FTP_TRANS_DIR_5": "LIST",
+ "total-payload-len": 131,
+ "name": "FTP_RESULT_1"
+ },
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "127.0.0.1.52584>127.0.0.1.6554",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_URI_4": "ftp://127.0.0.1/123.txt",
+ "FTP_TRANS_MODE_5": "PASV",
+ "FTP_TRANS_DIR_6": "RETR",
+ "total-payload-len": 44,
+ "name": "FTP_RESULT_2"
+ },
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "127.0.0.1.32799>127.0.0.1.58439",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_URI_4": "ftp://127.0.0.1/test.txt",
+ "FTP_TRANS_MODE_5": "PASV",
+ "FTP_TRANS_DIR_6": "STOR",
+ "total-payload-len": 140,
+ "name": "FTP_RESULT_3"
+ },
+ {
+ "FTP_LINK_TYPE": "CTRL",
+ "TUPLE4": "127.0.0.1.42042>127.0.0.1.21",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "FTP_ACCOUNT_2": "ftp",
+ "FTP_PASSWORD_3": "111111",
+ "FTP_TRANS_MODE_4": "PASV",
+ "FTP_TRANS_DIR_5": "LIST",
+ "FTP_TRANS_MODE_6": "PASV",
+ "FTP_TRANS_MODE_7": "PASV",
+ "FTP_URI_8": "ftp://127.0.0.1/123.txt",
+ "FTP_TRANS_DIR_9": "RETR",
+ "FTP_TRANS_MODE_10": "PASV",
+ "FTP_URI_11": "ftp://127.0.0.1/test.txt",
+ "FTP_TRANS_DIR_12": "STOR",
+ "name": "FTP_RESULT_4"
+ }
+] \ No newline at end of file
diff --git a/test/test_result_json/06-ftp_pasv-upload-download_C2S.json b/test/test_result_json/06-ftp_pasv-upload-download_C2S.json
new file mode 100644
index 0000000..30d28c7
--- /dev/null
+++ b/test/test_result_json/06-ftp_pasv-upload-download_C2S.json
@@ -0,0 +1,18 @@
+[
+ {
+ "FTP_LINK_TYPE": "CTRL",
+ "TUPLE4": "127.0.0.1.42042>127.0.0.1.21",
+ "FTP_ACCOUNT_1": "ftp",
+ "FTP_PASSWORD_2": "111111",
+ "FTP_TRANS_MODE_3": "PASV",
+ "FTP_TRANS_DIR_4": "LIST",
+ "FTP_TRANS_MODE_5": "PASV",
+ "FTP_TRANS_MODE_6": "PASV",
+ "FTP_URI_7": "ftp://127.0.0.1/123.txt",
+ "FTP_TRANS_DIR_8": "RETR",
+ "FTP_TRANS_MODE_9": "PASV",
+ "FTP_URI_10": "ftp://127.0.0.1/test.txt",
+ "FTP_TRANS_DIR_11": "STOR",
+ "name": "FTP_RESULT_0"
+ }
+] \ No newline at end of file
diff --git a/test/test_result_json/06-ftp_pasv-upload-download_S2C.json b/test/test_result_json/06-ftp_pasv-upload-download_S2C.json
new file mode 100644
index 0000000..86d0af6
--- /dev/null
+++ b/test/test_result_json/06-ftp_pasv-upload-download_S2C.json
@@ -0,0 +1,29 @@
+[
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "127.0.0.1.59814>127.0.0.1.13144",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "total-payload-len": 61,
+ "name": "FTP_RESULT_0"
+ },
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "127.0.0.1.38754>127.0.0.1.15810",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "total-payload-len": 131,
+ "name": "FTP_RESULT_1"
+ },
+ {
+ "FTP_LINK_TYPE": "DATA",
+ "TUPLE4": "127.0.0.1.52584>127.0.0.1.6554",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "total-payload-len": 44,
+ "name": "FTP_RESULT_2"
+ },
+ {
+ "FTP_LINK_TYPE": "CTRL",
+ "TUPLE4": "127.0.0.1.42042>127.0.0.1.21",
+ "FTP_BANNER_1": "Welcome to blah FTP service.",
+ "name": "FTP_RESULT_3"
+ }
+] \ No newline at end of file
diff --git a/test/test_result_json/empty.json b/test/test_result_json/empty.json
new file mode 100644
index 0000000..fe51488
--- /dev/null
+++ b/test/test_result_json/empty.json
@@ -0,0 +1 @@
+[]
diff --git a/test/uthash-2.3.0/include/utarray.h b/test/uthash-2.3.0/include/utarray.h
new file mode 100644
index 0000000..992fe7c
--- /dev/null
+++ b/test/uthash-2.3.0/include/utarray.h
@@ -0,0 +1,248 @@
+/*
+Copyright (c) 2008-2021, Troy D. Hanson http://troydhanson.github.com/uthash/
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+/* a dynamic array implementation using macros
+ */
+#ifndef UTARRAY_H
+#define UTARRAY_H
+
+#define UTARRAY_VERSION 2.3.0
+
+#include <stddef.h> /* size_t */
+#include <string.h> /* memset, etc */
+#include <stdlib.h> /* exit */
+
+#ifdef __GNUC__
+#define UTARRAY_UNUSED __attribute__((__unused__))
+#else
+#define UTARRAY_UNUSED
+#endif
+
+#ifdef oom
+#error "The name of macro 'oom' has been changed to 'utarray_oom'. Please update your code."
+#define utarray_oom() oom()
+#endif
+
+#ifndef utarray_oom
+#define utarray_oom() exit(-1)
+#endif
+
+typedef void (ctor_f)(void *dst, const void *src);
+typedef void (dtor_f)(void *elt);
+typedef void (init_f)(void *elt);
+typedef struct {
+ size_t sz;
+ init_f *init;
+ ctor_f *copy;
+ dtor_f *dtor;
+} UT_icd;
+
+typedef struct {
+ unsigned i,n;/* i: index of next available slot, n: num slots */
+ UT_icd icd; /* initializer, copy and destructor functions */
+ char *d; /* n slots of size icd->sz*/
+} UT_array;
+
+#define utarray_init(a,_icd) do { \
+ memset(a,0,sizeof(UT_array)); \
+ (a)->icd = *(_icd); \
+} while(0)
+
+#define utarray_done(a) do { \
+ if ((a)->n) { \
+ if ((a)->icd.dtor) { \
+ unsigned _ut_i; \
+ for(_ut_i=0; _ut_i < (a)->i; _ut_i++) { \
+ (a)->icd.dtor(utarray_eltptr(a,_ut_i)); \
+ } \
+ } \
+ free((a)->d); \
+ } \
+ (a)->n=0; \
+} while(0)
+
+#define utarray_new(a,_icd) do { \
+ (a) = (UT_array*)malloc(sizeof(UT_array)); \
+ if ((a) == NULL) { \
+ utarray_oom(); \
+ } \
+ utarray_init(a,_icd); \
+} while(0)
+
+#define utarray_free(a) do { \
+ utarray_done(a); \
+ free(a); \
+} while(0)
+
+#define utarray_reserve(a,by) do { \
+ if (((a)->i+(by)) > (a)->n) { \
+ char *utarray_tmp; \
+ while (((a)->i+(by)) > (a)->n) { (a)->n = ((a)->n ? (2*(a)->n) : 8); } \
+ utarray_tmp=(char*)realloc((a)->d, (a)->n*(a)->icd.sz); \
+ if (utarray_tmp == NULL) { \
+ utarray_oom(); \
+ } \
+ (a)->d=utarray_tmp; \
+ } \
+} while(0)
+
+#define utarray_push_back(a,p) do { \
+ utarray_reserve(a,1); \
+ if ((a)->icd.copy) { (a)->icd.copy( _utarray_eltptr(a,(a)->i++), p); } \
+ else { memcpy(_utarray_eltptr(a,(a)->i++), p, (a)->icd.sz); }; \
+} while(0)
+
+#define utarray_pop_back(a) do { \
+ if ((a)->icd.dtor) { (a)->icd.dtor( _utarray_eltptr(a,--((a)->i))); } \
+ else { (a)->i--; } \
+} while(0)
+
+#define utarray_extend_back(a) do { \
+ utarray_reserve(a,1); \
+ if ((a)->icd.init) { (a)->icd.init(_utarray_eltptr(a,(a)->i)); } \
+ else { memset(_utarray_eltptr(a,(a)->i),0,(a)->icd.sz); } \
+ (a)->i++; \
+} while(0)
+
+#define utarray_len(a) ((a)->i)
+
+#define utarray_eltptr(a,j) (((j) < (a)->i) ? _utarray_eltptr(a,j) : NULL)
+#define _utarray_eltptr(a,j) ((void*)((a)->d + ((a)->icd.sz * (j))))
+
+#define utarray_insert(a,p,j) do { \
+ if ((j) > (a)->i) utarray_resize(a,j); \
+ utarray_reserve(a,1); \
+ if ((j) < (a)->i) { \
+ memmove( _utarray_eltptr(a,(j)+1), _utarray_eltptr(a,j), \
+ ((a)->i - (j))*((a)->icd.sz)); \
+ } \
+ if ((a)->icd.copy) { (a)->icd.copy( _utarray_eltptr(a,j), p); } \
+ else { memcpy(_utarray_eltptr(a,j), p, (a)->icd.sz); }; \
+ (a)->i++; \
+} while(0)
+
+#define utarray_inserta(a,w,j) do { \
+ if (utarray_len(w) == 0) break; \
+ if ((j) > (a)->i) utarray_resize(a,j); \
+ utarray_reserve(a,utarray_len(w)); \
+ if ((j) < (a)->i) { \
+ memmove(_utarray_eltptr(a,(j)+utarray_len(w)), \
+ _utarray_eltptr(a,j), \
+ ((a)->i - (j))*((a)->icd.sz)); \
+ } \
+ if ((a)->icd.copy) { \
+ unsigned _ut_i; \
+ for(_ut_i=0;_ut_i<(w)->i;_ut_i++) { \
+ (a)->icd.copy(_utarray_eltptr(a, (j) + _ut_i), _utarray_eltptr(w, _ut_i)); \
+ } \
+ } else { \
+ memcpy(_utarray_eltptr(a,j), _utarray_eltptr(w,0), \
+ utarray_len(w)*((a)->icd.sz)); \
+ } \
+ (a)->i += utarray_len(w); \
+} while(0)
+
+#define utarray_resize(dst,num) do { \
+ unsigned _ut_i; \
+ if ((dst)->i > (unsigned)(num)) { \
+ if ((dst)->icd.dtor) { \
+ for (_ut_i = (num); _ut_i < (dst)->i; ++_ut_i) { \
+ (dst)->icd.dtor(_utarray_eltptr(dst, _ut_i)); \
+ } \
+ } \
+ } else if ((dst)->i < (unsigned)(num)) { \
+ utarray_reserve(dst, (num) - (dst)->i); \
+ if ((dst)->icd.init) { \
+ for (_ut_i = (dst)->i; _ut_i < (unsigned)(num); ++_ut_i) { \
+ (dst)->icd.init(_utarray_eltptr(dst, _ut_i)); \
+ } \
+ } else { \
+ memset(_utarray_eltptr(dst, (dst)->i), 0, (dst)->icd.sz*((num) - (dst)->i)); \
+ } \
+ } \
+ (dst)->i = (num); \
+} while(0)
+
+#define utarray_concat(dst,src) do { \
+ utarray_inserta(dst, src, utarray_len(dst)); \
+} while(0)
+
+#define utarray_erase(a,pos,len) do { \
+ if ((a)->icd.dtor) { \
+ unsigned _ut_i; \
+ for (_ut_i = 0; _ut_i < (len); _ut_i++) { \
+ (a)->icd.dtor(utarray_eltptr(a, (pos) + _ut_i)); \
+ } \
+ } \
+ if ((a)->i > ((pos) + (len))) { \
+ memmove(_utarray_eltptr(a, pos), _utarray_eltptr(a, (pos) + (len)), \
+ ((a)->i - ((pos) + (len))) * (a)->icd.sz); \
+ } \
+ (a)->i -= (len); \
+} while(0)
+
+#define utarray_renew(a,u) do { \
+ if (a) utarray_clear(a); \
+ else utarray_new(a, u); \
+} while(0)
+
+#define utarray_clear(a) do { \
+ if ((a)->i > 0) { \
+ if ((a)->icd.dtor) { \
+ unsigned _ut_i; \
+ for(_ut_i=0; _ut_i < (a)->i; _ut_i++) { \
+ (a)->icd.dtor(_utarray_eltptr(a, _ut_i)); \
+ } \
+ } \
+ (a)->i = 0; \
+ } \
+} while(0)
+
+#define utarray_sort(a,cmp) do { \
+ qsort((a)->d, (a)->i, (a)->icd.sz, cmp); \
+} while(0)
+
+#define utarray_find(a,v,cmp) bsearch((v),(a)->d,(a)->i,(a)->icd.sz,cmp)
+
+#define utarray_front(a) (((a)->i) ? (_utarray_eltptr(a,0)) : NULL)
+#define utarray_next(a,e) (((e)==NULL) ? utarray_front(a) : (((a)->i != utarray_eltidx(a,e)+1) ? _utarray_eltptr(a,utarray_eltidx(a,e)+1) : NULL))
+#define utarray_prev(a,e) (((e)==NULL) ? utarray_back(a) : ((utarray_eltidx(a,e) != 0) ? _utarray_eltptr(a,utarray_eltidx(a,e)-1) : NULL))
+#define utarray_back(a) (((a)->i) ? (_utarray_eltptr(a,(a)->i-1)) : NULL)
+#define utarray_eltidx(a,e) (((char*)(e) - (a)->d) / (a)->icd.sz)
+
+/* last we pre-define a few icd for common utarrays of ints and strings */
+static void utarray_str_cpy(void *dst, const void *src) {
+ char *const *srcc = (char *const *)src;
+ char **dstc = (char**)dst;
+ *dstc = (*srcc == NULL) ? NULL : strdup(*srcc);
+}
+static void utarray_str_dtor(void *elt) {
+ char **eltc = (char**)elt;
+ if (*eltc != NULL) free(*eltc);
+}
+static const UT_icd ut_str_icd UTARRAY_UNUSED = {sizeof(char*),NULL,utarray_str_cpy,utarray_str_dtor};
+static const UT_icd ut_int_icd UTARRAY_UNUSED = {sizeof(int),NULL,NULL,NULL};
+static const UT_icd ut_ptr_icd UTARRAY_UNUSED = {sizeof(void*),NULL,NULL,NULL};
+
+
+#endif /* UTARRAY_H */
diff --git a/test/uthash-2.3.0/include/uthash.h b/test/uthash-2.3.0/include/uthash.h
new file mode 100644
index 0000000..ac78fda
--- /dev/null
+++ b/test/uthash-2.3.0/include/uthash.h
@@ -0,0 +1,1136 @@
+/*
+Copyright (c) 2003-2021, Troy D. Hanson http://troydhanson.github.com/uthash/
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifndef UTHASH_H
+#define UTHASH_H
+
+#define UTHASH_VERSION 2.3.0
+
+#include <string.h> /* memcmp, memset, strlen */
+#include <stddef.h> /* ptrdiff_t */
+#include <stdlib.h> /* exit */
+
+#if defined(HASH_DEFINE_OWN_STDINT) && HASH_DEFINE_OWN_STDINT
+/* This codepath is provided for backward compatibility, but I plan to remove it. */
+#warning "HASH_DEFINE_OWN_STDINT is deprecated; please use HASH_NO_STDINT instead"
+typedef unsigned int uint32_t;
+typedef unsigned char uint8_t;
+#elif defined(HASH_NO_STDINT) && HASH_NO_STDINT
+#else
+#include <stdint.h> /* uint8_t, uint32_t */
+#endif
+
+/* These macros use decltype or the earlier __typeof GNU extension.
+ As decltype is only available in newer compilers (VS2010 or gcc 4.3+
+ when compiling c++ source) this code uses whatever method is needed
+ or, for VS2008 where neither is available, uses casting workarounds. */
+#if !defined(DECLTYPE) && !defined(NO_DECLTYPE)
+#if defined(_MSC_VER) /* MS compiler */
+#if _MSC_VER >= 1600 && defined(__cplusplus) /* VS2010 or newer in C++ mode */
+#define DECLTYPE(x) (decltype(x))
+#else /* VS2008 or older (or VS2010 in C mode) */
+#define NO_DECLTYPE
+#endif
+#elif defined(__BORLANDC__) || defined(__ICCARM__) || defined(__LCC__) || defined(__WATCOMC__)
+#define NO_DECLTYPE
+#else /* GNU, Sun and other compilers */
+#define DECLTYPE(x) (__typeof(x))
+#endif
+#endif
+
+#ifdef NO_DECLTYPE
+#define DECLTYPE(x)
+#define DECLTYPE_ASSIGN(dst,src) \
+do { \
+ char **_da_dst = (char**)(&(dst)); \
+ *_da_dst = (char*)(src); \
+} while (0)
+#else
+#define DECLTYPE_ASSIGN(dst,src) \
+do { \
+ (dst) = DECLTYPE(dst)(src); \
+} while (0)
+#endif
+
+#ifndef uthash_malloc
+#define uthash_malloc(sz) malloc(sz) /* malloc fcn */
+#endif
+#ifndef uthash_free
+#define uthash_free(ptr,sz) free(ptr) /* free fcn */
+#endif
+#ifndef uthash_bzero
+#define uthash_bzero(a,n) memset(a,'\0',n)
+#endif
+#ifndef uthash_strlen
+#define uthash_strlen(s) strlen(s)
+#endif
+
+#ifndef HASH_FUNCTION
+#define HASH_FUNCTION(keyptr,keylen,hashv) HASH_JEN(keyptr, keylen, hashv)
+#endif
+
+#ifndef HASH_KEYCMP
+#define HASH_KEYCMP(a,b,n) memcmp(a,b,n)
+#endif
+
+#ifndef uthash_noexpand_fyi
+#define uthash_noexpand_fyi(tbl) /* can be defined to log noexpand */
+#endif
+#ifndef uthash_expand_fyi
+#define uthash_expand_fyi(tbl) /* can be defined to log expands */
+#endif
+
+#ifndef HASH_NONFATAL_OOM
+#define HASH_NONFATAL_OOM 0
+#endif
+
+#if HASH_NONFATAL_OOM
+/* malloc failures can be recovered from */
+
+#ifndef uthash_nonfatal_oom
+#define uthash_nonfatal_oom(obj) do {} while (0) /* non-fatal OOM error */
+#endif
+
+#define HASH_RECORD_OOM(oomed) do { (oomed) = 1; } while (0)
+#define IF_HASH_NONFATAL_OOM(x) x
+
+#else
+/* malloc failures result in lost memory, hash tables are unusable */
+
+#ifndef uthash_fatal
+#define uthash_fatal(msg) exit(-1) /* fatal OOM error */
+#endif
+
+#define HASH_RECORD_OOM(oomed) uthash_fatal("out of memory")
+#define IF_HASH_NONFATAL_OOM(x)
+
+#endif
+
+/* initial number of buckets */
+#define HASH_INITIAL_NUM_BUCKETS 32U /* initial number of buckets */
+#define HASH_INITIAL_NUM_BUCKETS_LOG2 5U /* lg2 of initial number of buckets */
+#define HASH_BKT_CAPACITY_THRESH 10U /* expand when bucket count reaches */
+
+/* calculate the element whose hash handle address is hhp */
+#define ELMT_FROM_HH(tbl,hhp) ((void*)(((char*)(hhp)) - ((tbl)->hho)))
+/* calculate the hash handle from element address elp */
+#define HH_FROM_ELMT(tbl,elp) ((UT_hash_handle*)(void*)(((char*)(elp)) + ((tbl)->hho)))
+
+#define HASH_ROLLBACK_BKT(hh, head, itemptrhh) \
+do { \
+ struct UT_hash_handle *_hd_hh_item = (itemptrhh); \
+ unsigned _hd_bkt; \
+ HASH_TO_BKT(_hd_hh_item->hashv, (head)->hh.tbl->num_buckets, _hd_bkt); \
+ (head)->hh.tbl->buckets[_hd_bkt].count++; \
+ _hd_hh_item->hh_next = NULL; \
+ _hd_hh_item->hh_prev = NULL; \
+} while (0)
+
+#define HASH_VALUE(keyptr,keylen,hashv) \
+do { \
+ HASH_FUNCTION(keyptr, keylen, hashv); \
+} while (0)
+
+#define HASH_FIND_BYHASHVALUE(hh,head,keyptr,keylen,hashval,out) \
+do { \
+ (out) = NULL; \
+ if (head) { \
+ unsigned _hf_bkt; \
+ HASH_TO_BKT(hashval, (head)->hh.tbl->num_buckets, _hf_bkt); \
+ if (HASH_BLOOM_TEST((head)->hh.tbl, hashval) != 0) { \
+ HASH_FIND_IN_BKT((head)->hh.tbl, hh, (head)->hh.tbl->buckets[ _hf_bkt ], keyptr, keylen, hashval, out); \
+ } \
+ } \
+} while (0)
+
+#define HASH_FIND(hh,head,keyptr,keylen,out) \
+do { \
+ (out) = NULL; \
+ if (head) { \
+ unsigned _hf_hashv; \
+ HASH_VALUE(keyptr, keylen, _hf_hashv); \
+ HASH_FIND_BYHASHVALUE(hh, head, keyptr, keylen, _hf_hashv, out); \
+ } \
+} while (0)
+
+#ifdef HASH_BLOOM
+#define HASH_BLOOM_BITLEN (1UL << HASH_BLOOM)
+#define HASH_BLOOM_BYTELEN (HASH_BLOOM_BITLEN/8UL) + (((HASH_BLOOM_BITLEN%8UL)!=0UL) ? 1UL : 0UL)
+#define HASH_BLOOM_MAKE(tbl,oomed) \
+do { \
+ (tbl)->bloom_nbits = HASH_BLOOM; \
+ (tbl)->bloom_bv = (uint8_t*)uthash_malloc(HASH_BLOOM_BYTELEN); \
+ if (!(tbl)->bloom_bv) { \
+ HASH_RECORD_OOM(oomed); \
+ } else { \
+ uthash_bzero((tbl)->bloom_bv, HASH_BLOOM_BYTELEN); \
+ (tbl)->bloom_sig = HASH_BLOOM_SIGNATURE; \
+ } \
+} while (0)
+
+#define HASH_BLOOM_FREE(tbl) \
+do { \
+ uthash_free((tbl)->bloom_bv, HASH_BLOOM_BYTELEN); \
+} while (0)
+
+#define HASH_BLOOM_BITSET(bv,idx) (bv[(idx)/8U] |= (1U << ((idx)%8U)))
+#define HASH_BLOOM_BITTEST(bv,idx) (bv[(idx)/8U] & (1U << ((idx)%8U)))
+
+#define HASH_BLOOM_ADD(tbl,hashv) \
+ HASH_BLOOM_BITSET((tbl)->bloom_bv, ((hashv) & (uint32_t)((1UL << (tbl)->bloom_nbits) - 1U)))
+
+#define HASH_BLOOM_TEST(tbl,hashv) \
+ HASH_BLOOM_BITTEST((tbl)->bloom_bv, ((hashv) & (uint32_t)((1UL << (tbl)->bloom_nbits) - 1U)))
+
+#else
+#define HASH_BLOOM_MAKE(tbl,oomed)
+#define HASH_BLOOM_FREE(tbl)
+#define HASH_BLOOM_ADD(tbl,hashv)
+#define HASH_BLOOM_TEST(tbl,hashv) (1)
+#define HASH_BLOOM_BYTELEN 0U
+#endif
+
+#define HASH_MAKE_TABLE(hh,head,oomed) \
+do { \
+ (head)->hh.tbl = (UT_hash_table*)uthash_malloc(sizeof(UT_hash_table)); \
+ if (!(head)->hh.tbl) { \
+ HASH_RECORD_OOM(oomed); \
+ } else { \
+ uthash_bzero((head)->hh.tbl, sizeof(UT_hash_table)); \
+ (head)->hh.tbl->tail = &((head)->hh); \
+ (head)->hh.tbl->num_buckets = HASH_INITIAL_NUM_BUCKETS; \
+ (head)->hh.tbl->log2_num_buckets = HASH_INITIAL_NUM_BUCKETS_LOG2; \
+ (head)->hh.tbl->hho = (char*)(&(head)->hh) - (char*)(head); \
+ (head)->hh.tbl->buckets = (UT_hash_bucket*)uthash_malloc( \
+ HASH_INITIAL_NUM_BUCKETS * sizeof(struct UT_hash_bucket)); \
+ (head)->hh.tbl->signature = HASH_SIGNATURE; \
+ if (!(head)->hh.tbl->buckets) { \
+ HASH_RECORD_OOM(oomed); \
+ uthash_free((head)->hh.tbl, sizeof(UT_hash_table)); \
+ } else { \
+ uthash_bzero((head)->hh.tbl->buckets, \
+ HASH_INITIAL_NUM_BUCKETS * sizeof(struct UT_hash_bucket)); \
+ HASH_BLOOM_MAKE((head)->hh.tbl, oomed); \
+ IF_HASH_NONFATAL_OOM( \
+ if (oomed) { \
+ uthash_free((head)->hh.tbl->buckets, \
+ HASH_INITIAL_NUM_BUCKETS*sizeof(struct UT_hash_bucket)); \
+ uthash_free((head)->hh.tbl, sizeof(UT_hash_table)); \
+ } \
+ ) \
+ } \
+ } \
+} while (0)
+
+#define HASH_REPLACE_BYHASHVALUE_INORDER(hh,head,fieldname,keylen_in,hashval,add,replaced,cmpfcn) \
+do { \
+ (replaced) = NULL; \
+ HASH_FIND_BYHASHVALUE(hh, head, &((add)->fieldname), keylen_in, hashval, replaced); \
+ if (replaced) { \
+ HASH_DELETE(hh, head, replaced); \
+ } \
+ HASH_ADD_KEYPTR_BYHASHVALUE_INORDER(hh, head, &((add)->fieldname), keylen_in, hashval, add, cmpfcn); \
+} while (0)
+
+#define HASH_REPLACE_BYHASHVALUE(hh,head,fieldname,keylen_in,hashval,add,replaced) \
+do { \
+ (replaced) = NULL; \
+ HASH_FIND_BYHASHVALUE(hh, head, &((add)->fieldname), keylen_in, hashval, replaced); \
+ if (replaced) { \
+ HASH_DELETE(hh, head, replaced); \
+ } \
+ HASH_ADD_KEYPTR_BYHASHVALUE(hh, head, &((add)->fieldname), keylen_in, hashval, add); \
+} while (0)
+
+#define HASH_REPLACE(hh,head,fieldname,keylen_in,add,replaced) \
+do { \
+ unsigned _hr_hashv; \
+ HASH_VALUE(&((add)->fieldname), keylen_in, _hr_hashv); \
+ HASH_REPLACE_BYHASHVALUE(hh, head, fieldname, keylen_in, _hr_hashv, add, replaced); \
+} while (0)
+
+#define HASH_REPLACE_INORDER(hh,head,fieldname,keylen_in,add,replaced,cmpfcn) \
+do { \
+ unsigned _hr_hashv; \
+ HASH_VALUE(&((add)->fieldname), keylen_in, _hr_hashv); \
+ HASH_REPLACE_BYHASHVALUE_INORDER(hh, head, fieldname, keylen_in, _hr_hashv, add, replaced, cmpfcn); \
+} while (0)
+
+#define HASH_APPEND_LIST(hh, head, add) \
+do { \
+ (add)->hh.next = NULL; \
+ (add)->hh.prev = ELMT_FROM_HH((head)->hh.tbl, (head)->hh.tbl->tail); \
+ (head)->hh.tbl->tail->next = (add); \
+ (head)->hh.tbl->tail = &((add)->hh); \
+} while (0)
+
+#define HASH_AKBI_INNER_LOOP(hh,head,add,cmpfcn) \
+do { \
+ do { \
+ if (cmpfcn(DECLTYPE(head)(_hs_iter), add) > 0) { \
+ break; \
+ } \
+ } while ((_hs_iter = HH_FROM_ELMT((head)->hh.tbl, _hs_iter)->next)); \
+} while (0)
+
+#ifdef NO_DECLTYPE
+#undef HASH_AKBI_INNER_LOOP
+#define HASH_AKBI_INNER_LOOP(hh,head,add,cmpfcn) \
+do { \
+ char *_hs_saved_head = (char*)(head); \
+ do { \
+ DECLTYPE_ASSIGN(head, _hs_iter); \
+ if (cmpfcn(head, add) > 0) { \
+ DECLTYPE_ASSIGN(head, _hs_saved_head); \
+ break; \
+ } \
+ DECLTYPE_ASSIGN(head, _hs_saved_head); \
+ } while ((_hs_iter = HH_FROM_ELMT((head)->hh.tbl, _hs_iter)->next)); \
+} while (0)
+#endif
+
+#if HASH_NONFATAL_OOM
+
+#define HASH_ADD_TO_TABLE(hh,head,keyptr,keylen_in,hashval,add,oomed) \
+do { \
+ if (!(oomed)) { \
+ unsigned _ha_bkt; \
+ (head)->hh.tbl->num_items++; \
+ HASH_TO_BKT(hashval, (head)->hh.tbl->num_buckets, _ha_bkt); \
+ HASH_ADD_TO_BKT((head)->hh.tbl->buckets[_ha_bkt], hh, &(add)->hh, oomed); \
+ if (oomed) { \
+ HASH_ROLLBACK_BKT(hh, head, &(add)->hh); \
+ HASH_DELETE_HH(hh, head, &(add)->hh); \
+ (add)->hh.tbl = NULL; \
+ uthash_nonfatal_oom(add); \
+ } else { \
+ HASH_BLOOM_ADD((head)->hh.tbl, hashval); \
+ HASH_EMIT_KEY(hh, head, keyptr, keylen_in); \
+ } \
+ } else { \
+ (add)->hh.tbl = NULL; \
+ uthash_nonfatal_oom(add); \
+ } \
+} while (0)
+
+#else
+
+#define HASH_ADD_TO_TABLE(hh,head,keyptr,keylen_in,hashval,add,oomed) \
+do { \
+ unsigned _ha_bkt; \
+ (head)->hh.tbl->num_items++; \
+ HASH_TO_BKT(hashval, (head)->hh.tbl->num_buckets, _ha_bkt); \
+ HASH_ADD_TO_BKT((head)->hh.tbl->buckets[_ha_bkt], hh, &(add)->hh, oomed); \
+ HASH_BLOOM_ADD((head)->hh.tbl, hashval); \
+ HASH_EMIT_KEY(hh, head, keyptr, keylen_in); \
+} while (0)
+
+#endif
+
+
+#define HASH_ADD_KEYPTR_BYHASHVALUE_INORDER(hh,head,keyptr,keylen_in,hashval,add,cmpfcn) \
+do { \
+ IF_HASH_NONFATAL_OOM( int _ha_oomed = 0; ) \
+ (add)->hh.hashv = (hashval); \
+ (add)->hh.key = (char*) (keyptr); \
+ (add)->hh.keylen = (unsigned) (keylen_in); \
+ if (!(head)) { \
+ (add)->hh.next = NULL; \
+ (add)->hh.prev = NULL; \
+ HASH_MAKE_TABLE(hh, add, _ha_oomed); \
+ IF_HASH_NONFATAL_OOM( if (!_ha_oomed) { ) \
+ (head) = (add); \
+ IF_HASH_NONFATAL_OOM( } ) \
+ } else { \
+ void *_hs_iter = (head); \
+ (add)->hh.tbl = (head)->hh.tbl; \
+ HASH_AKBI_INNER_LOOP(hh, head, add, cmpfcn); \
+ if (_hs_iter) { \
+ (add)->hh.next = _hs_iter; \
+ if (((add)->hh.prev = HH_FROM_ELMT((head)->hh.tbl, _hs_iter)->prev)) { \
+ HH_FROM_ELMT((head)->hh.tbl, (add)->hh.prev)->next = (add); \
+ } else { \
+ (head) = (add); \
+ } \
+ HH_FROM_ELMT((head)->hh.tbl, _hs_iter)->prev = (add); \
+ } else { \
+ HASH_APPEND_LIST(hh, head, add); \
+ } \
+ } \
+ HASH_ADD_TO_TABLE(hh, head, keyptr, keylen_in, hashval, add, _ha_oomed); \
+ HASH_FSCK(hh, head, "HASH_ADD_KEYPTR_BYHASHVALUE_INORDER"); \
+} while (0)
+
+#define HASH_ADD_KEYPTR_INORDER(hh,head,keyptr,keylen_in,add,cmpfcn) \
+do { \
+ unsigned _hs_hashv; \
+ HASH_VALUE(keyptr, keylen_in, _hs_hashv); \
+ HASH_ADD_KEYPTR_BYHASHVALUE_INORDER(hh, head, keyptr, keylen_in, _hs_hashv, add, cmpfcn); \
+} while (0)
+
+#define HASH_ADD_BYHASHVALUE_INORDER(hh,head,fieldname,keylen_in,hashval,add,cmpfcn) \
+ HASH_ADD_KEYPTR_BYHASHVALUE_INORDER(hh, head, &((add)->fieldname), keylen_in, hashval, add, cmpfcn)
+
+#define HASH_ADD_INORDER(hh,head,fieldname,keylen_in,add,cmpfcn) \
+ HASH_ADD_KEYPTR_INORDER(hh, head, &((add)->fieldname), keylen_in, add, cmpfcn)
+
+#define HASH_ADD_KEYPTR_BYHASHVALUE(hh,head,keyptr,keylen_in,hashval,add) \
+do { \
+ IF_HASH_NONFATAL_OOM( int _ha_oomed = 0; ) \
+ (add)->hh.hashv = (hashval); \
+ (add)->hh.key = (const void*) (keyptr); \
+ (add)->hh.keylen = (unsigned) (keylen_in); \
+ if (!(head)) { \
+ (add)->hh.next = NULL; \
+ (add)->hh.prev = NULL; \
+ HASH_MAKE_TABLE(hh, add, _ha_oomed); \
+ IF_HASH_NONFATAL_OOM( if (!_ha_oomed) { ) \
+ (head) = (add); \
+ IF_HASH_NONFATAL_OOM( } ) \
+ } else { \
+ (add)->hh.tbl = (head)->hh.tbl; \
+ HASH_APPEND_LIST(hh, head, add); \
+ } \
+ HASH_ADD_TO_TABLE(hh, head, keyptr, keylen_in, hashval, add, _ha_oomed); \
+ HASH_FSCK(hh, head, "HASH_ADD_KEYPTR_BYHASHVALUE"); \
+} while (0)
+
+#define HASH_ADD_KEYPTR(hh,head,keyptr,keylen_in,add) \
+do { \
+ unsigned _ha_hashv; \
+ HASH_VALUE(keyptr, keylen_in, _ha_hashv); \
+ HASH_ADD_KEYPTR_BYHASHVALUE(hh, head, keyptr, keylen_in, _ha_hashv, add); \
+} while (0)
+
+#define HASH_ADD_BYHASHVALUE(hh,head,fieldname,keylen_in,hashval,add) \
+ HASH_ADD_KEYPTR_BYHASHVALUE(hh, head, &((add)->fieldname), keylen_in, hashval, add)
+
+#define HASH_ADD(hh,head,fieldname,keylen_in,add) \
+ HASH_ADD_KEYPTR(hh, head, &((add)->fieldname), keylen_in, add)
+
+#define HASH_TO_BKT(hashv,num_bkts,bkt) \
+do { \
+ bkt = ((hashv) & ((num_bkts) - 1U)); \
+} while (0)
+
+/* delete "delptr" from the hash table.
+ * "the usual" patch-up process for the app-order doubly-linked-list.
+ * The use of _hd_hh_del below deserves special explanation.
+ * These used to be expressed using (delptr) but that led to a bug
+ * if someone used the same symbol for the head and deletee, like
+ * HASH_DELETE(hh,users,users);
+ * We want that to work, but by changing the head (users) below
+ * we were forfeiting our ability to further refer to the deletee (users)
+ * in the patch-up process. Solution: use scratch space to
+ * copy the deletee pointer, then the latter references are via that
+ * scratch pointer rather than through the repointed (users) symbol.
+ */
+#define HASH_DELETE(hh,head,delptr) \
+ HASH_DELETE_HH(hh, head, &(delptr)->hh)
+
+#define HASH_DELETE_HH(hh,head,delptrhh) \
+do { \
+ struct UT_hash_handle *_hd_hh_del = (delptrhh); \
+ if ((_hd_hh_del->prev == NULL) && (_hd_hh_del->next == NULL)) { \
+ HASH_BLOOM_FREE((head)->hh.tbl); \
+ uthash_free((head)->hh.tbl->buckets, \
+ (head)->hh.tbl->num_buckets * sizeof(struct UT_hash_bucket)); \
+ uthash_free((head)->hh.tbl, sizeof(UT_hash_table)); \
+ (head) = NULL; \
+ } else { \
+ unsigned _hd_bkt; \
+ if (_hd_hh_del == (head)->hh.tbl->tail) { \
+ (head)->hh.tbl->tail = HH_FROM_ELMT((head)->hh.tbl, _hd_hh_del->prev); \
+ } \
+ if (_hd_hh_del->prev != NULL) { \
+ HH_FROM_ELMT((head)->hh.tbl, _hd_hh_del->prev)->next = _hd_hh_del->next; \
+ } else { \
+ DECLTYPE_ASSIGN(head, _hd_hh_del->next); \
+ } \
+ if (_hd_hh_del->next != NULL) { \
+ HH_FROM_ELMT((head)->hh.tbl, _hd_hh_del->next)->prev = _hd_hh_del->prev; \
+ } \
+ HASH_TO_BKT(_hd_hh_del->hashv, (head)->hh.tbl->num_buckets, _hd_bkt); \
+ HASH_DEL_IN_BKT((head)->hh.tbl->buckets[_hd_bkt], _hd_hh_del); \
+ (head)->hh.tbl->num_items--; \
+ } \
+ HASH_FSCK(hh, head, "HASH_DELETE_HH"); \
+} while (0)
+
+/* convenience forms of HASH_FIND/HASH_ADD/HASH_DEL */
+#define HASH_FIND_STR(head,findstr,out) \
+do { \
+ unsigned _uthash_hfstr_keylen = (unsigned)uthash_strlen(findstr); \
+ HASH_FIND(hh, head, findstr, _uthash_hfstr_keylen, out); \
+} while (0)
+#define HASH_ADD_STR(head,strfield,add) \
+do { \
+ unsigned _uthash_hastr_keylen = (unsigned)uthash_strlen((add)->strfield); \
+ HASH_ADD(hh, head, strfield[0], _uthash_hastr_keylen, add); \
+} while (0)
+#define HASH_REPLACE_STR(head,strfield,add,replaced) \
+do { \
+ unsigned _uthash_hrstr_keylen = (unsigned)uthash_strlen((add)->strfield); \
+ HASH_REPLACE(hh, head, strfield[0], _uthash_hrstr_keylen, add, replaced); \
+} while (0)
+#define HASH_FIND_INT(head,findint,out) \
+ HASH_FIND(hh,head,findint,sizeof(int),out)
+#define HASH_ADD_INT(head,intfield,add) \
+ HASH_ADD(hh,head,intfield,sizeof(int),add)
+#define HASH_REPLACE_INT(head,intfield,add,replaced) \
+ HASH_REPLACE(hh,head,intfield,sizeof(int),add,replaced)
+#define HASH_FIND_PTR(head,findptr,out) \
+ HASH_FIND(hh,head,findptr,sizeof(void *),out)
+#define HASH_ADD_PTR(head,ptrfield,add) \
+ HASH_ADD(hh,head,ptrfield,sizeof(void *),add)
+#define HASH_REPLACE_PTR(head,ptrfield,add,replaced) \
+ HASH_REPLACE(hh,head,ptrfield,sizeof(void *),add,replaced)
+#define HASH_DEL(head,delptr) \
+ HASH_DELETE(hh,head,delptr)
+
+/* HASH_FSCK checks hash integrity on every add/delete when HASH_DEBUG is defined.
+ * This is for uthash developer only; it compiles away if HASH_DEBUG isn't defined.
+ */
+#ifdef HASH_DEBUG
+#include <stdio.h> /* fprintf, stderr */
+#define HASH_OOPS(...) do { fprintf(stderr, __VA_ARGS__); exit(-1); } while (0)
+#define HASH_FSCK(hh,head,where) \
+do { \
+ struct UT_hash_handle *_thh; \
+ if (head) { \
+ unsigned _bkt_i; \
+ unsigned _count = 0; \
+ char *_prev; \
+ for (_bkt_i = 0; _bkt_i < (head)->hh.tbl->num_buckets; ++_bkt_i) { \
+ unsigned _bkt_count = 0; \
+ _thh = (head)->hh.tbl->buckets[_bkt_i].hh_head; \
+ _prev = NULL; \
+ while (_thh) { \
+ if (_prev != (char*)(_thh->hh_prev)) { \
+ HASH_OOPS("%s: invalid hh_prev %p, actual %p\n", \
+ (where), (void*)_thh->hh_prev, (void*)_prev); \
+ } \
+ _bkt_count++; \
+ _prev = (char*)(_thh); \
+ _thh = _thh->hh_next; \
+ } \
+ _count += _bkt_count; \
+ if ((head)->hh.tbl->buckets[_bkt_i].count != _bkt_count) { \
+ HASH_OOPS("%s: invalid bucket count %u, actual %u\n", \
+ (where), (head)->hh.tbl->buckets[_bkt_i].count, _bkt_count); \
+ } \
+ } \
+ if (_count != (head)->hh.tbl->num_items) { \
+ HASH_OOPS("%s: invalid hh item count %u, actual %u\n", \
+ (where), (head)->hh.tbl->num_items, _count); \
+ } \
+ _count = 0; \
+ _prev = NULL; \
+ _thh = &(head)->hh; \
+ while (_thh) { \
+ _count++; \
+ if (_prev != (char*)_thh->prev) { \
+ HASH_OOPS("%s: invalid prev %p, actual %p\n", \
+ (where), (void*)_thh->prev, (void*)_prev); \
+ } \
+ _prev = (char*)ELMT_FROM_HH((head)->hh.tbl, _thh); \
+ _thh = (_thh->next ? HH_FROM_ELMT((head)->hh.tbl, _thh->next) : NULL); \
+ } \
+ if (_count != (head)->hh.tbl->num_items) { \
+ HASH_OOPS("%s: invalid app item count %u, actual %u\n", \
+ (where), (head)->hh.tbl->num_items, _count); \
+ } \
+ } \
+} while (0)
+#else
+#define HASH_FSCK(hh,head,where)
+#endif
+
+/* When compiled with -DHASH_EMIT_KEYS, length-prefixed keys are emitted to
+ * the descriptor to which this macro is defined for tuning the hash function.
+ * The app can #include <unistd.h> to get the prototype for write(2). */
+#ifdef HASH_EMIT_KEYS
+#define HASH_EMIT_KEY(hh,head,keyptr,fieldlen) \
+do { \
+ unsigned _klen = fieldlen; \
+ write(HASH_EMIT_KEYS, &_klen, sizeof(_klen)); \
+ write(HASH_EMIT_KEYS, keyptr, (unsigned long)fieldlen); \
+} while (0)
+#else
+#define HASH_EMIT_KEY(hh,head,keyptr,fieldlen)
+#endif
+
+/* The Bernstein hash function, used in Perl prior to v5.6. Note (x<<5+x)=x*33. */
+#define HASH_BER(key,keylen,hashv) \
+do { \
+ unsigned _hb_keylen = (unsigned)keylen; \
+ const unsigned char *_hb_key = (const unsigned char*)(key); \
+ (hashv) = 0; \
+ while (_hb_keylen-- != 0U) { \
+ (hashv) = (((hashv) << 5) + (hashv)) + *_hb_key++; \
+ } \
+} while (0)
+
+
+/* SAX/FNV/OAT/JEN hash functions are macro variants of those listed at
+ * http://eternallyconfuzzled.com/tuts/algorithms/jsw_tut_hashing.aspx */
+#define HASH_SAX(key,keylen,hashv) \
+do { \
+ unsigned _sx_i; \
+ const unsigned char *_hs_key = (const unsigned char*)(key); \
+ hashv = 0; \
+ for (_sx_i=0; _sx_i < keylen; _sx_i++) { \
+ hashv ^= (hashv << 5) + (hashv >> 2) + _hs_key[_sx_i]; \
+ } \
+} while (0)
+/* FNV-1a variation */
+#define HASH_FNV(key,keylen,hashv) \
+do { \
+ unsigned _fn_i; \
+ const unsigned char *_hf_key = (const unsigned char*)(key); \
+ (hashv) = 2166136261U; \
+ for (_fn_i=0; _fn_i < keylen; _fn_i++) { \
+ hashv = hashv ^ _hf_key[_fn_i]; \
+ hashv = hashv * 16777619U; \
+ } \
+} while (0)
+
+#define HASH_OAT(key,keylen,hashv) \
+do { \
+ unsigned _ho_i; \
+ const unsigned char *_ho_key=(const unsigned char*)(key); \
+ hashv = 0; \
+ for(_ho_i=0; _ho_i < keylen; _ho_i++) { \
+ hashv += _ho_key[_ho_i]; \
+ hashv += (hashv << 10); \
+ hashv ^= (hashv >> 6); \
+ } \
+ hashv += (hashv << 3); \
+ hashv ^= (hashv >> 11); \
+ hashv += (hashv << 15); \
+} while (0)
+
+#define HASH_JEN_MIX(a,b,c) \
+do { \
+ a -= b; a -= c; a ^= ( c >> 13 ); \
+ b -= c; b -= a; b ^= ( a << 8 ); \
+ c -= a; c -= b; c ^= ( b >> 13 ); \
+ a -= b; a -= c; a ^= ( c >> 12 ); \
+ b -= c; b -= a; b ^= ( a << 16 ); \
+ c -= a; c -= b; c ^= ( b >> 5 ); \
+ a -= b; a -= c; a ^= ( c >> 3 ); \
+ b -= c; b -= a; b ^= ( a << 10 ); \
+ c -= a; c -= b; c ^= ( b >> 15 ); \
+} while (0)
+
+#define HASH_JEN(key,keylen,hashv) \
+do { \
+ unsigned _hj_i,_hj_j,_hj_k; \
+ unsigned const char *_hj_key=(unsigned const char*)(key); \
+ hashv = 0xfeedbeefu; \
+ _hj_i = _hj_j = 0x9e3779b9u; \
+ _hj_k = (unsigned)(keylen); \
+ while (_hj_k >= 12U) { \
+ _hj_i += (_hj_key[0] + ( (unsigned)_hj_key[1] << 8 ) \
+ + ( (unsigned)_hj_key[2] << 16 ) \
+ + ( (unsigned)_hj_key[3] << 24 ) ); \
+ _hj_j += (_hj_key[4] + ( (unsigned)_hj_key[5] << 8 ) \
+ + ( (unsigned)_hj_key[6] << 16 ) \
+ + ( (unsigned)_hj_key[7] << 24 ) ); \
+ hashv += (_hj_key[8] + ( (unsigned)_hj_key[9] << 8 ) \
+ + ( (unsigned)_hj_key[10] << 16 ) \
+ + ( (unsigned)_hj_key[11] << 24 ) ); \
+ \
+ HASH_JEN_MIX(_hj_i, _hj_j, hashv); \
+ \
+ _hj_key += 12; \
+ _hj_k -= 12U; \
+ } \
+ hashv += (unsigned)(keylen); \
+ switch ( _hj_k ) { \
+ case 11: hashv += ( (unsigned)_hj_key[10] << 24 ); /* FALLTHROUGH */ \
+ case 10: hashv += ( (unsigned)_hj_key[9] << 16 ); /* FALLTHROUGH */ \
+ case 9: hashv += ( (unsigned)_hj_key[8] << 8 ); /* FALLTHROUGH */ \
+ case 8: _hj_j += ( (unsigned)_hj_key[7] << 24 ); /* FALLTHROUGH */ \
+ case 7: _hj_j += ( (unsigned)_hj_key[6] << 16 ); /* FALLTHROUGH */ \
+ case 6: _hj_j += ( (unsigned)_hj_key[5] << 8 ); /* FALLTHROUGH */ \
+ case 5: _hj_j += _hj_key[4]; /* FALLTHROUGH */ \
+ case 4: _hj_i += ( (unsigned)_hj_key[3] << 24 ); /* FALLTHROUGH */ \
+ case 3: _hj_i += ( (unsigned)_hj_key[2] << 16 ); /* FALLTHROUGH */ \
+ case 2: _hj_i += ( (unsigned)_hj_key[1] << 8 ); /* FALLTHROUGH */ \
+ case 1: _hj_i += _hj_key[0]; /* FALLTHROUGH */ \
+ default: ; \
+ } \
+ HASH_JEN_MIX(_hj_i, _hj_j, hashv); \
+} while (0)
+
+/* The Paul Hsieh hash function */
+#undef get16bits
+#if (defined(__GNUC__) && defined(__i386__)) || defined(__WATCOMC__) \
+ || defined(_MSC_VER) || defined (__BORLANDC__) || defined (__TURBOC__)
+#define get16bits(d) (*((const uint16_t *) (d)))
+#endif
+
+#if !defined (get16bits)
+#define get16bits(d) ((((uint32_t)(((const uint8_t *)(d))[1])) << 8) \
+ +(uint32_t)(((const uint8_t *)(d))[0]) )
+#endif
+#define HASH_SFH(key,keylen,hashv) \
+do { \
+ unsigned const char *_sfh_key=(unsigned const char*)(key); \
+ uint32_t _sfh_tmp, _sfh_len = (uint32_t)keylen; \
+ \
+ unsigned _sfh_rem = _sfh_len & 3U; \
+ _sfh_len >>= 2; \
+ hashv = 0xcafebabeu; \
+ \
+ /* Main loop */ \
+ for (;_sfh_len > 0U; _sfh_len--) { \
+ hashv += get16bits (_sfh_key); \
+ _sfh_tmp = ((uint32_t)(get16bits (_sfh_key+2)) << 11) ^ hashv; \
+ hashv = (hashv << 16) ^ _sfh_tmp; \
+ _sfh_key += 2U*sizeof (uint16_t); \
+ hashv += hashv >> 11; \
+ } \
+ \
+ /* Handle end cases */ \
+ switch (_sfh_rem) { \
+ case 3: hashv += get16bits (_sfh_key); \
+ hashv ^= hashv << 16; \
+ hashv ^= (uint32_t)(_sfh_key[sizeof (uint16_t)]) << 18; \
+ hashv += hashv >> 11; \
+ break; \
+ case 2: hashv += get16bits (_sfh_key); \
+ hashv ^= hashv << 11; \
+ hashv += hashv >> 17; \
+ break; \
+ case 1: hashv += *_sfh_key; \
+ hashv ^= hashv << 10; \
+ hashv += hashv >> 1; \
+ break; \
+ default: ; \
+ } \
+ \
+ /* Force "avalanching" of final 127 bits */ \
+ hashv ^= hashv << 3; \
+ hashv += hashv >> 5; \
+ hashv ^= hashv << 4; \
+ hashv += hashv >> 17; \
+ hashv ^= hashv << 25; \
+ hashv += hashv >> 6; \
+} while (0)
+
+/* iterate over items in a known bucket to find desired item */
+#define HASH_FIND_IN_BKT(tbl,hh,head,keyptr,keylen_in,hashval,out) \
+do { \
+ if ((head).hh_head != NULL) { \
+ DECLTYPE_ASSIGN(out, ELMT_FROM_HH(tbl, (head).hh_head)); \
+ } else { \
+ (out) = NULL; \
+ } \
+ while ((out) != NULL) { \
+ if ((out)->hh.hashv == (hashval) && (out)->hh.keylen == (keylen_in)) { \
+ if (HASH_KEYCMP((out)->hh.key, keyptr, keylen_in) == 0) { \
+ break; \
+ } \
+ } \
+ if ((out)->hh.hh_next != NULL) { \
+ DECLTYPE_ASSIGN(out, ELMT_FROM_HH(tbl, (out)->hh.hh_next)); \
+ } else { \
+ (out) = NULL; \
+ } \
+ } \
+} while (0)
+
+/* add an item to a bucket */
+#define HASH_ADD_TO_BKT(head,hh,addhh,oomed) \
+do { \
+ UT_hash_bucket *_ha_head = &(head); \
+ _ha_head->count++; \
+ (addhh)->hh_next = _ha_head->hh_head; \
+ (addhh)->hh_prev = NULL; \
+ if (_ha_head->hh_head != NULL) { \
+ _ha_head->hh_head->hh_prev = (addhh); \
+ } \
+ _ha_head->hh_head = (addhh); \
+ if ((_ha_head->count >= ((_ha_head->expand_mult + 1U) * HASH_BKT_CAPACITY_THRESH)) \
+ && !(addhh)->tbl->noexpand) { \
+ HASH_EXPAND_BUCKETS(addhh,(addhh)->tbl, oomed); \
+ IF_HASH_NONFATAL_OOM( \
+ if (oomed) { \
+ HASH_DEL_IN_BKT(head,addhh); \
+ } \
+ ) \
+ } \
+} while (0)
+
+/* remove an item from a given bucket */
+#define HASH_DEL_IN_BKT(head,delhh) \
+do { \
+ UT_hash_bucket *_hd_head = &(head); \
+ _hd_head->count--; \
+ if (_hd_head->hh_head == (delhh)) { \
+ _hd_head->hh_head = (delhh)->hh_next; \
+ } \
+ if ((delhh)->hh_prev) { \
+ (delhh)->hh_prev->hh_next = (delhh)->hh_next; \
+ } \
+ if ((delhh)->hh_next) { \
+ (delhh)->hh_next->hh_prev = (delhh)->hh_prev; \
+ } \
+} while (0)
+
+/* Bucket expansion has the effect of doubling the number of buckets
+ * and redistributing the items into the new buckets. Ideally the
+ * items will distribute more or less evenly into the new buckets
+ * (the extent to which this is true is a measure of the quality of
+ * the hash function as it applies to the key domain).
+ *
+ * With the items distributed into more buckets, the chain length
+ * (item count) in each bucket is reduced. Thus by expanding buckets
+ * the hash keeps a bound on the chain length. This bounded chain
+ * length is the essence of how a hash provides constant time lookup.
+ *
+ * The calculation of tbl->ideal_chain_maxlen below deserves some
+ * explanation. First, keep in mind that we're calculating the ideal
+ * maximum chain length based on the *new* (doubled) bucket count.
+ * In fractions this is just n/b (n=number of items,b=new num buckets).
+ * Since the ideal chain length is an integer, we want to calculate
+ * ceil(n/b). We don't depend on floating point arithmetic in this
+ * hash, so to calculate ceil(n/b) with integers we could write
+ *
+ * ceil(n/b) = (n/b) + ((n%b)?1:0)
+ *
+ * and in fact a previous version of this hash did just that.
+ * But now we have improved things a bit by recognizing that b is
+ * always a power of two. We keep its base 2 log handy (call it lb),
+ * so now we can write this with a bit shift and logical AND:
+ *
+ * ceil(n/b) = (n>>lb) + ( (n & (b-1)) ? 1:0)
+ *
+ */
+#define HASH_EXPAND_BUCKETS(hh,tbl,oomed) \
+do { \
+ unsigned _he_bkt; \
+ unsigned _he_bkt_i; \
+ struct UT_hash_handle *_he_thh, *_he_hh_nxt; \
+ UT_hash_bucket *_he_new_buckets, *_he_newbkt; \
+ _he_new_buckets = (UT_hash_bucket*)uthash_malloc( \
+ sizeof(struct UT_hash_bucket) * (tbl)->num_buckets * 2U); \
+ if (!_he_new_buckets) { \
+ HASH_RECORD_OOM(oomed); \
+ } else { \
+ uthash_bzero(_he_new_buckets, \
+ sizeof(struct UT_hash_bucket) * (tbl)->num_buckets * 2U); \
+ (tbl)->ideal_chain_maxlen = \
+ ((tbl)->num_items >> ((tbl)->log2_num_buckets+1U)) + \
+ ((((tbl)->num_items & (((tbl)->num_buckets*2U)-1U)) != 0U) ? 1U : 0U); \
+ (tbl)->nonideal_items = 0; \
+ for (_he_bkt_i = 0; _he_bkt_i < (tbl)->num_buckets; _he_bkt_i++) { \
+ _he_thh = (tbl)->buckets[ _he_bkt_i ].hh_head; \
+ while (_he_thh != NULL) { \
+ _he_hh_nxt = _he_thh->hh_next; \
+ HASH_TO_BKT(_he_thh->hashv, (tbl)->num_buckets * 2U, _he_bkt); \
+ _he_newbkt = &(_he_new_buckets[_he_bkt]); \
+ if (++(_he_newbkt->count) > (tbl)->ideal_chain_maxlen) { \
+ (tbl)->nonideal_items++; \
+ if (_he_newbkt->count > _he_newbkt->expand_mult * (tbl)->ideal_chain_maxlen) { \
+ _he_newbkt->expand_mult++; \
+ } \
+ } \
+ _he_thh->hh_prev = NULL; \
+ _he_thh->hh_next = _he_newbkt->hh_head; \
+ if (_he_newbkt->hh_head != NULL) { \
+ _he_newbkt->hh_head->hh_prev = _he_thh; \
+ } \
+ _he_newbkt->hh_head = _he_thh; \
+ _he_thh = _he_hh_nxt; \
+ } \
+ } \
+ uthash_free((tbl)->buckets, (tbl)->num_buckets * sizeof(struct UT_hash_bucket)); \
+ (tbl)->num_buckets *= 2U; \
+ (tbl)->log2_num_buckets++; \
+ (tbl)->buckets = _he_new_buckets; \
+ (tbl)->ineff_expands = ((tbl)->nonideal_items > ((tbl)->num_items >> 1)) ? \
+ ((tbl)->ineff_expands+1U) : 0U; \
+ if ((tbl)->ineff_expands > 1U) { \
+ (tbl)->noexpand = 1; \
+ uthash_noexpand_fyi(tbl); \
+ } \
+ uthash_expand_fyi(tbl); \
+ } \
+} while (0)
+
+
+/* This is an adaptation of Simon Tatham's O(n log(n)) mergesort */
+/* Note that HASH_SORT assumes the hash handle name to be hh.
+ * HASH_SRT was added to allow the hash handle name to be passed in. */
+#define HASH_SORT(head,cmpfcn) HASH_SRT(hh,head,cmpfcn)
+#define HASH_SRT(hh,head,cmpfcn) \
+do { \
+ unsigned _hs_i; \
+ unsigned _hs_looping,_hs_nmerges,_hs_insize,_hs_psize,_hs_qsize; \
+ struct UT_hash_handle *_hs_p, *_hs_q, *_hs_e, *_hs_list, *_hs_tail; \
+ if (head != NULL) { \
+ _hs_insize = 1; \
+ _hs_looping = 1; \
+ _hs_list = &((head)->hh); \
+ while (_hs_looping != 0U) { \
+ _hs_p = _hs_list; \
+ _hs_list = NULL; \
+ _hs_tail = NULL; \
+ _hs_nmerges = 0; \
+ while (_hs_p != NULL) { \
+ _hs_nmerges++; \
+ _hs_q = _hs_p; \
+ _hs_psize = 0; \
+ for (_hs_i = 0; _hs_i < _hs_insize; ++_hs_i) { \
+ _hs_psize++; \
+ _hs_q = ((_hs_q->next != NULL) ? \
+ HH_FROM_ELMT((head)->hh.tbl, _hs_q->next) : NULL); \
+ if (_hs_q == NULL) { \
+ break; \
+ } \
+ } \
+ _hs_qsize = _hs_insize; \
+ while ((_hs_psize != 0U) || ((_hs_qsize != 0U) && (_hs_q != NULL))) { \
+ if (_hs_psize == 0U) { \
+ _hs_e = _hs_q; \
+ _hs_q = ((_hs_q->next != NULL) ? \
+ HH_FROM_ELMT((head)->hh.tbl, _hs_q->next) : NULL); \
+ _hs_qsize--; \
+ } else if ((_hs_qsize == 0U) || (_hs_q == NULL)) { \
+ _hs_e = _hs_p; \
+ if (_hs_p != NULL) { \
+ _hs_p = ((_hs_p->next != NULL) ? \
+ HH_FROM_ELMT((head)->hh.tbl, _hs_p->next) : NULL); \
+ } \
+ _hs_psize--; \
+ } else if ((cmpfcn( \
+ DECLTYPE(head)(ELMT_FROM_HH((head)->hh.tbl, _hs_p)), \
+ DECLTYPE(head)(ELMT_FROM_HH((head)->hh.tbl, _hs_q)) \
+ )) <= 0) { \
+ _hs_e = _hs_p; \
+ if (_hs_p != NULL) { \
+ _hs_p = ((_hs_p->next != NULL) ? \
+ HH_FROM_ELMT((head)->hh.tbl, _hs_p->next) : NULL); \
+ } \
+ _hs_psize--; \
+ } else { \
+ _hs_e = _hs_q; \
+ _hs_q = ((_hs_q->next != NULL) ? \
+ HH_FROM_ELMT((head)->hh.tbl, _hs_q->next) : NULL); \
+ _hs_qsize--; \
+ } \
+ if ( _hs_tail != NULL ) { \
+ _hs_tail->next = ((_hs_e != NULL) ? \
+ ELMT_FROM_HH((head)->hh.tbl, _hs_e) : NULL); \
+ } else { \
+ _hs_list = _hs_e; \
+ } \
+ if (_hs_e != NULL) { \
+ _hs_e->prev = ((_hs_tail != NULL) ? \
+ ELMT_FROM_HH((head)->hh.tbl, _hs_tail) : NULL); \
+ } \
+ _hs_tail = _hs_e; \
+ } \
+ _hs_p = _hs_q; \
+ } \
+ if (_hs_tail != NULL) { \
+ _hs_tail->next = NULL; \
+ } \
+ if (_hs_nmerges <= 1U) { \
+ _hs_looping = 0; \
+ (head)->hh.tbl->tail = _hs_tail; \
+ DECLTYPE_ASSIGN(head, ELMT_FROM_HH((head)->hh.tbl, _hs_list)); \
+ } \
+ _hs_insize *= 2U; \
+ } \
+ HASH_FSCK(hh, head, "HASH_SRT"); \
+ } \
+} while (0)
+
+/* This function selects items from one hash into another hash.
+ * The end result is that the selected items have dual presence
+ * in both hashes. There is no copy of the items made; rather
+ * they are added into the new hash through a secondary hash
+ * hash handle that must be present in the structure. */
+#define HASH_SELECT(hh_dst, dst, hh_src, src, cond) \
+do { \
+ unsigned _src_bkt, _dst_bkt; \
+ void *_last_elt = NULL, *_elt; \
+ UT_hash_handle *_src_hh, *_dst_hh, *_last_elt_hh=NULL; \
+ ptrdiff_t _dst_hho = ((char*)(&(dst)->hh_dst) - (char*)(dst)); \
+ if ((src) != NULL) { \
+ for (_src_bkt=0; _src_bkt < (src)->hh_src.tbl->num_buckets; _src_bkt++) { \
+ for (_src_hh = (src)->hh_src.tbl->buckets[_src_bkt].hh_head; \
+ _src_hh != NULL; \
+ _src_hh = _src_hh->hh_next) { \
+ _elt = ELMT_FROM_HH((src)->hh_src.tbl, _src_hh); \
+ if (cond(_elt)) { \
+ IF_HASH_NONFATAL_OOM( int _hs_oomed = 0; ) \
+ _dst_hh = (UT_hash_handle*)(void*)(((char*)_elt) + _dst_hho); \
+ _dst_hh->key = _src_hh->key; \
+ _dst_hh->keylen = _src_hh->keylen; \
+ _dst_hh->hashv = _src_hh->hashv; \
+ _dst_hh->prev = _last_elt; \
+ _dst_hh->next = NULL; \
+ if (_last_elt_hh != NULL) { \
+ _last_elt_hh->next = _elt; \
+ } \
+ if ((dst) == NULL) { \
+ DECLTYPE_ASSIGN(dst, _elt); \
+ HASH_MAKE_TABLE(hh_dst, dst, _hs_oomed); \
+ IF_HASH_NONFATAL_OOM( \
+ if (_hs_oomed) { \
+ uthash_nonfatal_oom(_elt); \
+ (dst) = NULL; \
+ continue; \
+ } \
+ ) \
+ } else { \
+ _dst_hh->tbl = (dst)->hh_dst.tbl; \
+ } \
+ HASH_TO_BKT(_dst_hh->hashv, _dst_hh->tbl->num_buckets, _dst_bkt); \
+ HASH_ADD_TO_BKT(_dst_hh->tbl->buckets[_dst_bkt], hh_dst, _dst_hh, _hs_oomed); \
+ (dst)->hh_dst.tbl->num_items++; \
+ IF_HASH_NONFATAL_OOM( \
+ if (_hs_oomed) { \
+ HASH_ROLLBACK_BKT(hh_dst, dst, _dst_hh); \
+ HASH_DELETE_HH(hh_dst, dst, _dst_hh); \
+ _dst_hh->tbl = NULL; \
+ uthash_nonfatal_oom(_elt); \
+ continue; \
+ } \
+ ) \
+ HASH_BLOOM_ADD(_dst_hh->tbl, _dst_hh->hashv); \
+ _last_elt = _elt; \
+ _last_elt_hh = _dst_hh; \
+ } \
+ } \
+ } \
+ } \
+ HASH_FSCK(hh_dst, dst, "HASH_SELECT"); \
+} while (0)
+
+#define HASH_CLEAR(hh,head) \
+do { \
+ if ((head) != NULL) { \
+ HASH_BLOOM_FREE((head)->hh.tbl); \
+ uthash_free((head)->hh.tbl->buckets, \
+ (head)->hh.tbl->num_buckets*sizeof(struct UT_hash_bucket)); \
+ uthash_free((head)->hh.tbl, sizeof(UT_hash_table)); \
+ (head) = NULL; \
+ } \
+} while (0)
+
+#define HASH_OVERHEAD(hh,head) \
+ (((head) != NULL) ? ( \
+ (size_t)(((head)->hh.tbl->num_items * sizeof(UT_hash_handle)) + \
+ ((head)->hh.tbl->num_buckets * sizeof(UT_hash_bucket)) + \
+ sizeof(UT_hash_table) + \
+ (HASH_BLOOM_BYTELEN))) : 0U)
+
+#ifdef NO_DECLTYPE
+#define HASH_ITER(hh,head,el,tmp) \
+for(((el)=(head)), ((*(char**)(&(tmp)))=(char*)((head!=NULL)?(head)->hh.next:NULL)); \
+ (el) != NULL; ((el)=(tmp)), ((*(char**)(&(tmp)))=(char*)((tmp!=NULL)?(tmp)->hh.next:NULL)))
+#else
+#define HASH_ITER(hh,head,el,tmp) \
+for(((el)=(head)), ((tmp)=DECLTYPE(el)((head!=NULL)?(head)->hh.next:NULL)); \
+ (el) != NULL; ((el)=(tmp)), ((tmp)=DECLTYPE(el)((tmp!=NULL)?(tmp)->hh.next:NULL)))
+#endif
+
+/* obtain a count of items in the hash */
+#define HASH_COUNT(head) HASH_CNT(hh,head)
+#define HASH_CNT(hh,head) ((head != NULL)?((head)->hh.tbl->num_items):0U)
+
+typedef struct UT_hash_bucket {
+ struct UT_hash_handle *hh_head;
+ unsigned count;
+
+ /* expand_mult is normally set to 0. In this situation, the max chain length
+ * threshold is enforced at its default value, HASH_BKT_CAPACITY_THRESH. (If
+ * the bucket's chain exceeds this length, bucket expansion is triggered).
+ * However, setting expand_mult to a non-zero value delays bucket expansion
+ * (that would be triggered by additions to this particular bucket)
+ * until its chain length reaches a *multiple* of HASH_BKT_CAPACITY_THRESH.
+ * (The multiplier is simply expand_mult+1). The whole idea of this
+ * multiplier is to reduce bucket expansions, since they are expensive, in
+ * situations where we know that a particular bucket tends to be overused.
+ * It is better to let its chain length grow to a longer yet-still-bounded
+ * value, than to do an O(n) bucket expansion too often.
+ */
+ unsigned expand_mult;
+
+} UT_hash_bucket;
+
+/* random signature used only to find hash tables in external analysis */
+#define HASH_SIGNATURE 0xa0111fe1u
+#define HASH_BLOOM_SIGNATURE 0xb12220f2u
+
+typedef struct UT_hash_table {
+ UT_hash_bucket *buckets;
+ unsigned num_buckets, log2_num_buckets;
+ unsigned num_items;
+ struct UT_hash_handle *tail; /* tail hh in app order, for fast append */
+ ptrdiff_t hho; /* hash handle offset (byte pos of hash handle in element */
+
+ /* in an ideal situation (all buckets used equally), no bucket would have
+ * more than ceil(#items/#buckets) items. that's the ideal chain length. */
+ unsigned ideal_chain_maxlen;
+
+ /* nonideal_items is the number of items in the hash whose chain position
+ * exceeds the ideal chain maxlen. these items pay the penalty for an uneven
+ * hash distribution; reaching them in a chain traversal takes >ideal steps */
+ unsigned nonideal_items;
+
+ /* ineffective expands occur when a bucket doubling was performed, but
+ * afterward, more than half the items in the hash had nonideal chain
+ * positions. If this happens on two consecutive expansions we inhibit any
+ * further expansion, as it's not helping; this happens when the hash
+ * function isn't a good fit for the key domain. When expansion is inhibited
+ * the hash will still work, albeit no longer in constant time. */
+ unsigned ineff_expands, noexpand;
+
+ uint32_t signature; /* used only to find hash tables in external analysis */
+#ifdef HASH_BLOOM
+ uint32_t bloom_sig; /* used only to test bloom exists in external analysis */
+ uint8_t *bloom_bv;
+ uint8_t bloom_nbits;
+#endif
+
+} UT_hash_table;
+
+typedef struct UT_hash_handle {
+ struct UT_hash_table *tbl;
+ void *prev; /* prev element in app order */
+ void *next; /* next element in app order */
+ struct UT_hash_handle *hh_prev; /* previous hh in bucket order */
+ struct UT_hash_handle *hh_next; /* next hh in bucket order */
+ const void *key; /* ptr to enclosing struct's key */
+ unsigned keylen; /* enclosing struct's key len */
+ unsigned hashv; /* result of hash-fcn(key) */
+} UT_hash_handle;
+
+#endif /* UTHASH_H */
diff --git a/test/uthash-2.3.0/include/utlist.h b/test/uthash-2.3.0/include/utlist.h
new file mode 100644
index 0000000..6230a67
--- /dev/null
+++ b/test/uthash-2.3.0/include/utlist.h
@@ -0,0 +1,1073 @@
+/*
+Copyright (c) 2007-2021, Troy D. Hanson http://troydhanson.github.com/uthash/
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifndef UTLIST_H
+#define UTLIST_H
+
+#define UTLIST_VERSION 2.3.0
+
+#include <assert.h>
+
+/*
+ * This file contains macros to manipulate singly and doubly-linked lists.
+ *
+ * 1. LL_ macros: singly-linked lists.
+ * 2. DL_ macros: doubly-linked lists.
+ * 3. CDL_ macros: circular doubly-linked lists.
+ *
+ * To use singly-linked lists, your structure must have a "next" pointer.
+ * To use doubly-linked lists, your structure must "prev" and "next" pointers.
+ * Either way, the pointer to the head of the list must be initialized to NULL.
+ *
+ * ----------------.EXAMPLE -------------------------
+ * struct item {
+ * int id;
+ * struct item *prev, *next;
+ * }
+ *
+ * struct item *list = NULL:
+ *
+ * int main() {
+ * struct item *item;
+ * ... allocate and populate item ...
+ * DL_APPEND(list, item);
+ * }
+ * --------------------------------------------------
+ *
+ * For doubly-linked lists, the append and delete macros are O(1)
+ * For singly-linked lists, append and delete are O(n) but prepend is O(1)
+ * The sort macro is O(n log(n)) for all types of single/double/circular lists.
+ */
+
+/* These macros use decltype or the earlier __typeof GNU extension.
+ As decltype is only available in newer compilers (VS2010 or gcc 4.3+
+ when compiling c++ source) this code uses whatever method is needed
+ or, for VS2008 where neither is available, uses casting workarounds. */
+#if !defined(LDECLTYPE) && !defined(NO_DECLTYPE)
+#if defined(_MSC_VER) /* MS compiler */
+#if _MSC_VER >= 1600 && defined(__cplusplus) /* VS2010 or newer in C++ mode */
+#define LDECLTYPE(x) decltype(x)
+#else /* VS2008 or older (or VS2010 in C mode) */
+#define NO_DECLTYPE
+#endif
+#elif defined(__BORLANDC__) || defined(__ICCARM__) || defined(__LCC__) || defined(__WATCOMC__)
+#define NO_DECLTYPE
+#else /* GNU, Sun and other compilers */
+#define LDECLTYPE(x) __typeof(x)
+#endif
+#endif
+
+/* for VS2008 we use some workarounds to get around the lack of decltype,
+ * namely, we always reassign our tmp variable to the list head if we need
+ * to dereference its prev/next pointers, and save/restore the real head.*/
+#ifdef NO_DECLTYPE
+#define IF_NO_DECLTYPE(x) x
+#define LDECLTYPE(x) char*
+#define UTLIST_SV(elt,list) _tmp = (char*)(list); {char **_alias = (char**)&(list); *_alias = (elt); }
+#define UTLIST_NEXT(elt,list,next) ((char*)((list)->next))
+#define UTLIST_NEXTASGN(elt,list,to,next) { char **_alias = (char**)&((list)->next); *_alias=(char*)(to); }
+/* #define UTLIST_PREV(elt,list,prev) ((char*)((list)->prev)) */
+#define UTLIST_PREVASGN(elt,list,to,prev) { char **_alias = (char**)&((list)->prev); *_alias=(char*)(to); }
+#define UTLIST_RS(list) { char **_alias = (char**)&(list); *_alias=_tmp; }
+#define UTLIST_CASTASGN(a,b) { char **_alias = (char**)&(a); *_alias=(char*)(b); }
+#else
+#define IF_NO_DECLTYPE(x)
+#define UTLIST_SV(elt,list)
+#define UTLIST_NEXT(elt,list,next) ((elt)->next)
+#define UTLIST_NEXTASGN(elt,list,to,next) ((elt)->next)=(to)
+/* #define UTLIST_PREV(elt,list,prev) ((elt)->prev) */
+#define UTLIST_PREVASGN(elt,list,to,prev) ((elt)->prev)=(to)
+#define UTLIST_RS(list)
+#define UTLIST_CASTASGN(a,b) (a)=(b)
+#endif
+
+/******************************************************************************
+ * The sort macro is an adaptation of Simon Tatham's O(n log(n)) mergesort *
+ * Unwieldy variable names used here to avoid shadowing passed-in variables. *
+ *****************************************************************************/
+#define LL_SORT(list, cmp) \
+ LL_SORT2(list, cmp, next)
+
+#define LL_SORT2(list, cmp, next) \
+do { \
+ LDECLTYPE(list) _ls_p; \
+ LDECLTYPE(list) _ls_q; \
+ LDECLTYPE(list) _ls_e; \
+ LDECLTYPE(list) _ls_tail; \
+ IF_NO_DECLTYPE(LDECLTYPE(list) _tmp;) \
+ int _ls_insize, _ls_nmerges, _ls_psize, _ls_qsize, _ls_i, _ls_looping; \
+ if (list) { \
+ _ls_insize = 1; \
+ _ls_looping = 1; \
+ while (_ls_looping) { \
+ UTLIST_CASTASGN(_ls_p,list); \
+ (list) = NULL; \
+ _ls_tail = NULL; \
+ _ls_nmerges = 0; \
+ while (_ls_p) { \
+ _ls_nmerges++; \
+ _ls_q = _ls_p; \
+ _ls_psize = 0; \
+ for (_ls_i = 0; _ls_i < _ls_insize; _ls_i++) { \
+ _ls_psize++; \
+ UTLIST_SV(_ls_q,list); _ls_q = UTLIST_NEXT(_ls_q,list,next); UTLIST_RS(list); \
+ if (!_ls_q) break; \
+ } \
+ _ls_qsize = _ls_insize; \
+ while (_ls_psize > 0 || (_ls_qsize > 0 && _ls_q)) { \
+ if (_ls_psize == 0) { \
+ _ls_e = _ls_q; UTLIST_SV(_ls_q,list); _ls_q = \
+ UTLIST_NEXT(_ls_q,list,next); UTLIST_RS(list); _ls_qsize--; \
+ } else if (_ls_qsize == 0 || !_ls_q) { \
+ _ls_e = _ls_p; UTLIST_SV(_ls_p,list); _ls_p = \
+ UTLIST_NEXT(_ls_p,list,next); UTLIST_RS(list); _ls_psize--; \
+ } else if (cmp(_ls_p,_ls_q) <= 0) { \
+ _ls_e = _ls_p; UTLIST_SV(_ls_p,list); _ls_p = \
+ UTLIST_NEXT(_ls_p,list,next); UTLIST_RS(list); _ls_psize--; \
+ } else { \
+ _ls_e = _ls_q; UTLIST_SV(_ls_q,list); _ls_q = \
+ UTLIST_NEXT(_ls_q,list,next); UTLIST_RS(list); _ls_qsize--; \
+ } \
+ if (_ls_tail) { \
+ UTLIST_SV(_ls_tail,list); UTLIST_NEXTASGN(_ls_tail,list,_ls_e,next); UTLIST_RS(list); \
+ } else { \
+ UTLIST_CASTASGN(list,_ls_e); \
+ } \
+ _ls_tail = _ls_e; \
+ } \
+ _ls_p = _ls_q; \
+ } \
+ if (_ls_tail) { \
+ UTLIST_SV(_ls_tail,list); UTLIST_NEXTASGN(_ls_tail,list,NULL,next); UTLIST_RS(list); \
+ } \
+ if (_ls_nmerges <= 1) { \
+ _ls_looping=0; \
+ } \
+ _ls_insize *= 2; \
+ } \
+ } \
+} while (0)
+
+
+#define DL_SORT(list, cmp) \
+ DL_SORT2(list, cmp, prev, next)
+
+#define DL_SORT2(list, cmp, prev, next) \
+do { \
+ LDECLTYPE(list) _ls_p; \
+ LDECLTYPE(list) _ls_q; \
+ LDECLTYPE(list) _ls_e; \
+ LDECLTYPE(list) _ls_tail; \
+ IF_NO_DECLTYPE(LDECLTYPE(list) _tmp;) \
+ int _ls_insize, _ls_nmerges, _ls_psize, _ls_qsize, _ls_i, _ls_looping; \
+ if (list) { \
+ _ls_insize = 1; \
+ _ls_looping = 1; \
+ while (_ls_looping) { \
+ UTLIST_CASTASGN(_ls_p,list); \
+ (list) = NULL; \
+ _ls_tail = NULL; \
+ _ls_nmerges = 0; \
+ while (_ls_p) { \
+ _ls_nmerges++; \
+ _ls_q = _ls_p; \
+ _ls_psize = 0; \
+ for (_ls_i = 0; _ls_i < _ls_insize; _ls_i++) { \
+ _ls_psize++; \
+ UTLIST_SV(_ls_q,list); _ls_q = UTLIST_NEXT(_ls_q,list,next); UTLIST_RS(list); \
+ if (!_ls_q) break; \
+ } \
+ _ls_qsize = _ls_insize; \
+ while ((_ls_psize > 0) || ((_ls_qsize > 0) && _ls_q)) { \
+ if (_ls_psize == 0) { \
+ _ls_e = _ls_q; UTLIST_SV(_ls_q,list); _ls_q = \
+ UTLIST_NEXT(_ls_q,list,next); UTLIST_RS(list); _ls_qsize--; \
+ } else if ((_ls_qsize == 0) || (!_ls_q)) { \
+ _ls_e = _ls_p; UTLIST_SV(_ls_p,list); _ls_p = \
+ UTLIST_NEXT(_ls_p,list,next); UTLIST_RS(list); _ls_psize--; \
+ } else if (cmp(_ls_p,_ls_q) <= 0) { \
+ _ls_e = _ls_p; UTLIST_SV(_ls_p,list); _ls_p = \
+ UTLIST_NEXT(_ls_p,list,next); UTLIST_RS(list); _ls_psize--; \
+ } else { \
+ _ls_e = _ls_q; UTLIST_SV(_ls_q,list); _ls_q = \
+ UTLIST_NEXT(_ls_q,list,next); UTLIST_RS(list); _ls_qsize--; \
+ } \
+ if (_ls_tail) { \
+ UTLIST_SV(_ls_tail,list); UTLIST_NEXTASGN(_ls_tail,list,_ls_e,next); UTLIST_RS(list); \
+ } else { \
+ UTLIST_CASTASGN(list,_ls_e); \
+ } \
+ UTLIST_SV(_ls_e,list); UTLIST_PREVASGN(_ls_e,list,_ls_tail,prev); UTLIST_RS(list); \
+ _ls_tail = _ls_e; \
+ } \
+ _ls_p = _ls_q; \
+ } \
+ UTLIST_CASTASGN((list)->prev, _ls_tail); \
+ UTLIST_SV(_ls_tail,list); UTLIST_NEXTASGN(_ls_tail,list,NULL,next); UTLIST_RS(list); \
+ if (_ls_nmerges <= 1) { \
+ _ls_looping=0; \
+ } \
+ _ls_insize *= 2; \
+ } \
+ } \
+} while (0)
+
+#define CDL_SORT(list, cmp) \
+ CDL_SORT2(list, cmp, prev, next)
+
+#define CDL_SORT2(list, cmp, prev, next) \
+do { \
+ LDECLTYPE(list) _ls_p; \
+ LDECLTYPE(list) _ls_q; \
+ LDECLTYPE(list) _ls_e; \
+ LDECLTYPE(list) _ls_tail; \
+ LDECLTYPE(list) _ls_oldhead; \
+ LDECLTYPE(list) _tmp; \
+ int _ls_insize, _ls_nmerges, _ls_psize, _ls_qsize, _ls_i, _ls_looping; \
+ if (list) { \
+ _ls_insize = 1; \
+ _ls_looping = 1; \
+ while (_ls_looping) { \
+ UTLIST_CASTASGN(_ls_p,list); \
+ UTLIST_CASTASGN(_ls_oldhead,list); \
+ (list) = NULL; \
+ _ls_tail = NULL; \
+ _ls_nmerges = 0; \
+ while (_ls_p) { \
+ _ls_nmerges++; \
+ _ls_q = _ls_p; \
+ _ls_psize = 0; \
+ for (_ls_i = 0; _ls_i < _ls_insize; _ls_i++) { \
+ _ls_psize++; \
+ UTLIST_SV(_ls_q,list); \
+ if (UTLIST_NEXT(_ls_q,list,next) == _ls_oldhead) { \
+ _ls_q = NULL; \
+ } else { \
+ _ls_q = UTLIST_NEXT(_ls_q,list,next); \
+ } \
+ UTLIST_RS(list); \
+ if (!_ls_q) break; \
+ } \
+ _ls_qsize = _ls_insize; \
+ while (_ls_psize > 0 || (_ls_qsize > 0 && _ls_q)) { \
+ if (_ls_psize == 0) { \
+ _ls_e = _ls_q; UTLIST_SV(_ls_q,list); _ls_q = \
+ UTLIST_NEXT(_ls_q,list,next); UTLIST_RS(list); _ls_qsize--; \
+ if (_ls_q == _ls_oldhead) { _ls_q = NULL; } \
+ } else if (_ls_qsize == 0 || !_ls_q) { \
+ _ls_e = _ls_p; UTLIST_SV(_ls_p,list); _ls_p = \
+ UTLIST_NEXT(_ls_p,list,next); UTLIST_RS(list); _ls_psize--; \
+ if (_ls_p == _ls_oldhead) { _ls_p = NULL; } \
+ } else if (cmp(_ls_p,_ls_q) <= 0) { \
+ _ls_e = _ls_p; UTLIST_SV(_ls_p,list); _ls_p = \
+ UTLIST_NEXT(_ls_p,list,next); UTLIST_RS(list); _ls_psize--; \
+ if (_ls_p == _ls_oldhead) { _ls_p = NULL; } \
+ } else { \
+ _ls_e = _ls_q; UTLIST_SV(_ls_q,list); _ls_q = \
+ UTLIST_NEXT(_ls_q,list,next); UTLIST_RS(list); _ls_qsize--; \
+ if (_ls_q == _ls_oldhead) { _ls_q = NULL; } \
+ } \
+ if (_ls_tail) { \
+ UTLIST_SV(_ls_tail,list); UTLIST_NEXTASGN(_ls_tail,list,_ls_e,next); UTLIST_RS(list); \
+ } else { \
+ UTLIST_CASTASGN(list,_ls_e); \
+ } \
+ UTLIST_SV(_ls_e,list); UTLIST_PREVASGN(_ls_e,list,_ls_tail,prev); UTLIST_RS(list); \
+ _ls_tail = _ls_e; \
+ } \
+ _ls_p = _ls_q; \
+ } \
+ UTLIST_CASTASGN((list)->prev,_ls_tail); \
+ UTLIST_CASTASGN(_tmp,list); \
+ UTLIST_SV(_ls_tail,list); UTLIST_NEXTASGN(_ls_tail,list,_tmp,next); UTLIST_RS(list); \
+ if (_ls_nmerges <= 1) { \
+ _ls_looping=0; \
+ } \
+ _ls_insize *= 2; \
+ } \
+ } \
+} while (0)
+
+/******************************************************************************
+ * singly linked list macros (non-circular) *
+ *****************************************************************************/
+#define LL_PREPEND(head,add) \
+ LL_PREPEND2(head,add,next)
+
+#define LL_PREPEND2(head,add,next) \
+do { \
+ (add)->next = (head); \
+ (head) = (add); \
+} while (0)
+
+#define LL_CONCAT(head1,head2) \
+ LL_CONCAT2(head1,head2,next)
+
+#define LL_CONCAT2(head1,head2,next) \
+do { \
+ LDECLTYPE(head1) _tmp; \
+ if (head1) { \
+ _tmp = (head1); \
+ while (_tmp->next) { _tmp = _tmp->next; } \
+ _tmp->next=(head2); \
+ } else { \
+ (head1)=(head2); \
+ } \
+} while (0)
+
+#define LL_APPEND(head,add) \
+ LL_APPEND2(head,add,next)
+
+#define LL_APPEND2(head,add,next) \
+do { \
+ LDECLTYPE(head) _tmp; \
+ (add)->next=NULL; \
+ if (head) { \
+ _tmp = (head); \
+ while (_tmp->next) { _tmp = _tmp->next; } \
+ _tmp->next=(add); \
+ } else { \
+ (head)=(add); \
+ } \
+} while (0)
+
+#define LL_INSERT_INORDER(head,add,cmp) \
+ LL_INSERT_INORDER2(head,add,cmp,next)
+
+#define LL_INSERT_INORDER2(head,add,cmp,next) \
+do { \
+ LDECLTYPE(head) _tmp; \
+ if (head) { \
+ LL_LOWER_BOUND2(head, _tmp, add, cmp, next); \
+ LL_APPEND_ELEM2(head, _tmp, add, next); \
+ } else { \
+ (head) = (add); \
+ (head)->next = NULL; \
+ } \
+} while (0)
+
+#define LL_LOWER_BOUND(head,elt,like,cmp) \
+ LL_LOWER_BOUND2(head,elt,like,cmp,next)
+
+#define LL_LOWER_BOUND2(head,elt,like,cmp,next) \
+ do { \
+ if ((head) == NULL || (cmp(head, like)) >= 0) { \
+ (elt) = NULL; \
+ } else { \
+ for ((elt) = (head); (elt)->next != NULL; (elt) = (elt)->next) { \
+ if (cmp((elt)->next, like) >= 0) { \
+ break; \
+ } \
+ } \
+ } \
+ } while (0)
+
+#define LL_DELETE(head,del) \
+ LL_DELETE2(head,del,next)
+
+#define LL_DELETE2(head,del,next) \
+do { \
+ LDECLTYPE(head) _tmp; \
+ if ((head) == (del)) { \
+ (head)=(head)->next; \
+ } else { \
+ _tmp = (head); \
+ while (_tmp->next && (_tmp->next != (del))) { \
+ _tmp = _tmp->next; \
+ } \
+ if (_tmp->next) { \
+ _tmp->next = (del)->next; \
+ } \
+ } \
+} while (0)
+
+#define LL_COUNT(head,el,counter) \
+ LL_COUNT2(head,el,counter,next) \
+
+#define LL_COUNT2(head,el,counter,next) \
+do { \
+ (counter) = 0; \
+ LL_FOREACH2(head,el,next) { ++(counter); } \
+} while (0)
+
+#define LL_FOREACH(head,el) \
+ LL_FOREACH2(head,el,next)
+
+#define LL_FOREACH2(head,el,next) \
+ for ((el) = (head); el; (el) = (el)->next)
+
+#define LL_FOREACH_SAFE(head,el,tmp) \
+ LL_FOREACH_SAFE2(head,el,tmp,next)
+
+#define LL_FOREACH_SAFE2(head,el,tmp,next) \
+ for ((el) = (head); (el) && ((tmp) = (el)->next, 1); (el) = (tmp))
+
+#define LL_SEARCH_SCALAR(head,out,field,val) \
+ LL_SEARCH_SCALAR2(head,out,field,val,next)
+
+#define LL_SEARCH_SCALAR2(head,out,field,val,next) \
+do { \
+ LL_FOREACH2(head,out,next) { \
+ if ((out)->field == (val)) break; \
+ } \
+} while (0)
+
+#define LL_SEARCH(head,out,elt,cmp) \
+ LL_SEARCH2(head,out,elt,cmp,next)
+
+#define LL_SEARCH2(head,out,elt,cmp,next) \
+do { \
+ LL_FOREACH2(head,out,next) { \
+ if ((cmp(out,elt))==0) break; \
+ } \
+} while (0)
+
+#define LL_REPLACE_ELEM2(head, el, add, next) \
+do { \
+ LDECLTYPE(head) _tmp; \
+ assert((head) != NULL); \
+ assert((el) != NULL); \
+ assert((add) != NULL); \
+ (add)->next = (el)->next; \
+ if ((head) == (el)) { \
+ (head) = (add); \
+ } else { \
+ _tmp = (head); \
+ while (_tmp->next && (_tmp->next != (el))) { \
+ _tmp = _tmp->next; \
+ } \
+ if (_tmp->next) { \
+ _tmp->next = (add); \
+ } \
+ } \
+} while (0)
+
+#define LL_REPLACE_ELEM(head, el, add) \
+ LL_REPLACE_ELEM2(head, el, add, next)
+
+#define LL_PREPEND_ELEM2(head, el, add, next) \
+do { \
+ if (el) { \
+ LDECLTYPE(head) _tmp; \
+ assert((head) != NULL); \
+ assert((add) != NULL); \
+ (add)->next = (el); \
+ if ((head) == (el)) { \
+ (head) = (add); \
+ } else { \
+ _tmp = (head); \
+ while (_tmp->next && (_tmp->next != (el))) { \
+ _tmp = _tmp->next; \
+ } \
+ if (_tmp->next) { \
+ _tmp->next = (add); \
+ } \
+ } \
+ } else { \
+ LL_APPEND2(head, add, next); \
+ } \
+} while (0) \
+
+#define LL_PREPEND_ELEM(head, el, add) \
+ LL_PREPEND_ELEM2(head, el, add, next)
+
+#define LL_APPEND_ELEM2(head, el, add, next) \
+do { \
+ if (el) { \
+ assert((head) != NULL); \
+ assert((add) != NULL); \
+ (add)->next = (el)->next; \
+ (el)->next = (add); \
+ } else { \
+ LL_PREPEND2(head, add, next); \
+ } \
+} while (0) \
+
+#define LL_APPEND_ELEM(head, el, add) \
+ LL_APPEND_ELEM2(head, el, add, next)
+
+#ifdef NO_DECLTYPE
+/* Here are VS2008 / NO_DECLTYPE replacements for a few functions */
+
+#undef LL_CONCAT2
+#define LL_CONCAT2(head1,head2,next) \
+do { \
+ char *_tmp; \
+ if (head1) { \
+ _tmp = (char*)(head1); \
+ while ((head1)->next) { (head1) = (head1)->next; } \
+ (head1)->next = (head2); \
+ UTLIST_RS(head1); \
+ } else { \
+ (head1)=(head2); \
+ } \
+} while (0)
+
+#undef LL_APPEND2
+#define LL_APPEND2(head,add,next) \
+do { \
+ if (head) { \
+ (add)->next = head; /* use add->next as a temp variable */ \
+ while ((add)->next->next) { (add)->next = (add)->next->next; } \
+ (add)->next->next=(add); \
+ } else { \
+ (head)=(add); \
+ } \
+ (add)->next=NULL; \
+} while (0)
+
+#undef LL_INSERT_INORDER2
+#define LL_INSERT_INORDER2(head,add,cmp,next) \
+do { \
+ if ((head) == NULL || (cmp(head, add)) >= 0) { \
+ (add)->next = (head); \
+ (head) = (add); \
+ } else { \
+ char *_tmp = (char*)(head); \
+ while ((head)->next != NULL && (cmp((head)->next, add)) < 0) { \
+ (head) = (head)->next; \
+ } \
+ (add)->next = (head)->next; \
+ (head)->next = (add); \
+ UTLIST_RS(head); \
+ } \
+} while (0)
+
+#undef LL_DELETE2
+#define LL_DELETE2(head,del,next) \
+do { \
+ if ((head) == (del)) { \
+ (head)=(head)->next; \
+ } else { \
+ char *_tmp = (char*)(head); \
+ while ((head)->next && ((head)->next != (del))) { \
+ (head) = (head)->next; \
+ } \
+ if ((head)->next) { \
+ (head)->next = ((del)->next); \
+ } \
+ UTLIST_RS(head); \
+ } \
+} while (0)
+
+#undef LL_REPLACE_ELEM2
+#define LL_REPLACE_ELEM2(head, el, add, next) \
+do { \
+ assert((head) != NULL); \
+ assert((el) != NULL); \
+ assert((add) != NULL); \
+ if ((head) == (el)) { \
+ (head) = (add); \
+ } else { \
+ (add)->next = head; \
+ while ((add)->next->next && ((add)->next->next != (el))) { \
+ (add)->next = (add)->next->next; \
+ } \
+ if ((add)->next->next) { \
+ (add)->next->next = (add); \
+ } \
+ } \
+ (add)->next = (el)->next; \
+} while (0)
+
+#undef LL_PREPEND_ELEM2
+#define LL_PREPEND_ELEM2(head, el, add, next) \
+do { \
+ if (el) { \
+ assert((head) != NULL); \
+ assert((add) != NULL); \
+ if ((head) == (el)) { \
+ (head) = (add); \
+ } else { \
+ (add)->next = (head); \
+ while ((add)->next->next && ((add)->next->next != (el))) { \
+ (add)->next = (add)->next->next; \
+ } \
+ if ((add)->next->next) { \
+ (add)->next->next = (add); \
+ } \
+ } \
+ (add)->next = (el); \
+ } else { \
+ LL_APPEND2(head, add, next); \
+ } \
+} while (0) \
+
+#endif /* NO_DECLTYPE */
+
+/******************************************************************************
+ * doubly linked list macros (non-circular) *
+ *****************************************************************************/
+#define DL_PREPEND(head,add) \
+ DL_PREPEND2(head,add,prev,next)
+
+#define DL_PREPEND2(head,add,prev,next) \
+do { \
+ (add)->next = (head); \
+ if (head) { \
+ (add)->prev = (head)->prev; \
+ (head)->prev = (add); \
+ } else { \
+ (add)->prev = (add); \
+ } \
+ (head) = (add); \
+} while (0)
+
+#define DL_APPEND(head,add) \
+ DL_APPEND2(head,add,prev,next)
+
+#define DL_APPEND2(head,add,prev,next) \
+do { \
+ if (head) { \
+ (add)->prev = (head)->prev; \
+ (head)->prev->next = (add); \
+ (head)->prev = (add); \
+ (add)->next = NULL; \
+ } else { \
+ (head)=(add); \
+ (head)->prev = (head); \
+ (head)->next = NULL; \
+ } \
+} while (0)
+
+#define DL_INSERT_INORDER(head,add,cmp) \
+ DL_INSERT_INORDER2(head,add,cmp,prev,next)
+
+#define DL_INSERT_INORDER2(head,add,cmp,prev,next) \
+do { \
+ LDECLTYPE(head) _tmp; \
+ if (head) { \
+ DL_LOWER_BOUND2(head, _tmp, add, cmp, next); \
+ DL_APPEND_ELEM2(head, _tmp, add, prev, next); \
+ } else { \
+ (head) = (add); \
+ (head)->prev = (head); \
+ (head)->next = NULL; \
+ } \
+} while (0)
+
+#define DL_LOWER_BOUND(head,elt,like,cmp) \
+ DL_LOWER_BOUND2(head,elt,like,cmp,next)
+
+#define DL_LOWER_BOUND2(head,elt,like,cmp,next) \
+do { \
+ if ((head) == NULL || (cmp(head, like)) >= 0) { \
+ (elt) = NULL; \
+ } else { \
+ for ((elt) = (head); (elt)->next != NULL; (elt) = (elt)->next) { \
+ if ((cmp((elt)->next, like)) >= 0) { \
+ break; \
+ } \
+ } \
+ } \
+} while (0)
+
+#define DL_CONCAT(head1,head2) \
+ DL_CONCAT2(head1,head2,prev,next)
+
+#define DL_CONCAT2(head1,head2,prev,next) \
+do { \
+ LDECLTYPE(head1) _tmp; \
+ if (head2) { \
+ if (head1) { \
+ UTLIST_CASTASGN(_tmp, (head2)->prev); \
+ (head2)->prev = (head1)->prev; \
+ (head1)->prev->next = (head2); \
+ UTLIST_CASTASGN((head1)->prev, _tmp); \
+ } else { \
+ (head1)=(head2); \
+ } \
+ } \
+} while (0)
+
+#define DL_DELETE(head,del) \
+ DL_DELETE2(head,del,prev,next)
+
+#define DL_DELETE2(head,del,prev,next) \
+do { \
+ assert((head) != NULL); \
+ assert((del)->prev != NULL); \
+ if ((del)->prev == (del)) { \
+ (head)=NULL; \
+ } else if ((del)==(head)) { \
+ (del)->next->prev = (del)->prev; \
+ (head) = (del)->next; \
+ } else { \
+ (del)->prev->next = (del)->next; \
+ if ((del)->next) { \
+ (del)->next->prev = (del)->prev; \
+ } else { \
+ (head)->prev = (del)->prev; \
+ } \
+ } \
+} while (0)
+
+#define DL_COUNT(head,el,counter) \
+ DL_COUNT2(head,el,counter,next) \
+
+#define DL_COUNT2(head,el,counter,next) \
+do { \
+ (counter) = 0; \
+ DL_FOREACH2(head,el,next) { ++(counter); } \
+} while (0)
+
+#define DL_FOREACH(head,el) \
+ DL_FOREACH2(head,el,next)
+
+#define DL_FOREACH2(head,el,next) \
+ for ((el) = (head); el; (el) = (el)->next)
+
+/* this version is safe for deleting the elements during iteration */
+#define DL_FOREACH_SAFE(head,el,tmp) \
+ DL_FOREACH_SAFE2(head,el,tmp,next)
+
+#define DL_FOREACH_SAFE2(head,el,tmp,next) \
+ for ((el) = (head); (el) && ((tmp) = (el)->next, 1); (el) = (tmp))
+
+/* these are identical to their singly-linked list counterparts */
+#define DL_SEARCH_SCALAR LL_SEARCH_SCALAR
+#define DL_SEARCH LL_SEARCH
+#define DL_SEARCH_SCALAR2 LL_SEARCH_SCALAR2
+#define DL_SEARCH2 LL_SEARCH2
+
+#define DL_REPLACE_ELEM2(head, el, add, prev, next) \
+do { \
+ assert((head) != NULL); \
+ assert((el) != NULL); \
+ assert((add) != NULL); \
+ if ((head) == (el)) { \
+ (head) = (add); \
+ (add)->next = (el)->next; \
+ if ((el)->next == NULL) { \
+ (add)->prev = (add); \
+ } else { \
+ (add)->prev = (el)->prev; \
+ (add)->next->prev = (add); \
+ } \
+ } else { \
+ (add)->next = (el)->next; \
+ (add)->prev = (el)->prev; \
+ (add)->prev->next = (add); \
+ if ((el)->next == NULL) { \
+ (head)->prev = (add); \
+ } else { \
+ (add)->next->prev = (add); \
+ } \
+ } \
+} while (0)
+
+#define DL_REPLACE_ELEM(head, el, add) \
+ DL_REPLACE_ELEM2(head, el, add, prev, next)
+
+#define DL_PREPEND_ELEM2(head, el, add, prev, next) \
+do { \
+ if (el) { \
+ assert((head) != NULL); \
+ assert((add) != NULL); \
+ (add)->next = (el); \
+ (add)->prev = (el)->prev; \
+ (el)->prev = (add); \
+ if ((head) == (el)) { \
+ (head) = (add); \
+ } else { \
+ (add)->prev->next = (add); \
+ } \
+ } else { \
+ DL_APPEND2(head, add, prev, next); \
+ } \
+} while (0) \
+
+#define DL_PREPEND_ELEM(head, el, add) \
+ DL_PREPEND_ELEM2(head, el, add, prev, next)
+
+#define DL_APPEND_ELEM2(head, el, add, prev, next) \
+do { \
+ if (el) { \
+ assert((head) != NULL); \
+ assert((add) != NULL); \
+ (add)->next = (el)->next; \
+ (add)->prev = (el); \
+ (el)->next = (add); \
+ if ((add)->next) { \
+ (add)->next->prev = (add); \
+ } else { \
+ (head)->prev = (add); \
+ } \
+ } else { \
+ DL_PREPEND2(head, add, prev, next); \
+ } \
+} while (0) \
+
+#define DL_APPEND_ELEM(head, el, add) \
+ DL_APPEND_ELEM2(head, el, add, prev, next)
+
+#ifdef NO_DECLTYPE
+/* Here are VS2008 / NO_DECLTYPE replacements for a few functions */
+
+#undef DL_INSERT_INORDER2
+#define DL_INSERT_INORDER2(head,add,cmp,prev,next) \
+do { \
+ if ((head) == NULL) { \
+ (add)->prev = (add); \
+ (add)->next = NULL; \
+ (head) = (add); \
+ } else if ((cmp(head, add)) >= 0) { \
+ (add)->prev = (head)->prev; \
+ (add)->next = (head); \
+ (head)->prev = (add); \
+ (head) = (add); \
+ } else { \
+ char *_tmp = (char*)(head); \
+ while ((head)->next && (cmp((head)->next, add)) < 0) { \
+ (head) = (head)->next; \
+ } \
+ (add)->prev = (head); \
+ (add)->next = (head)->next; \
+ (head)->next = (add); \
+ UTLIST_RS(head); \
+ if ((add)->next) { \
+ (add)->next->prev = (add); \
+ } else { \
+ (head)->prev = (add); \
+ } \
+ } \
+} while (0)
+#endif /* NO_DECLTYPE */
+
+/******************************************************************************
+ * circular doubly linked list macros *
+ *****************************************************************************/
+#define CDL_APPEND(head,add) \
+ CDL_APPEND2(head,add,prev,next)
+
+#define CDL_APPEND2(head,add,prev,next) \
+do { \
+ if (head) { \
+ (add)->prev = (head)->prev; \
+ (add)->next = (head); \
+ (head)->prev = (add); \
+ (add)->prev->next = (add); \
+ } else { \
+ (add)->prev = (add); \
+ (add)->next = (add); \
+ (head) = (add); \
+ } \
+} while (0)
+
+#define CDL_PREPEND(head,add) \
+ CDL_PREPEND2(head,add,prev,next)
+
+#define CDL_PREPEND2(head,add,prev,next) \
+do { \
+ if (head) { \
+ (add)->prev = (head)->prev; \
+ (add)->next = (head); \
+ (head)->prev = (add); \
+ (add)->prev->next = (add); \
+ } else { \
+ (add)->prev = (add); \
+ (add)->next = (add); \
+ } \
+ (head) = (add); \
+} while (0)
+
+#define CDL_INSERT_INORDER(head,add,cmp) \
+ CDL_INSERT_INORDER2(head,add,cmp,prev,next)
+
+#define CDL_INSERT_INORDER2(head,add,cmp,prev,next) \
+do { \
+ LDECLTYPE(head) _tmp; \
+ if (head) { \
+ CDL_LOWER_BOUND2(head, _tmp, add, cmp, next); \
+ CDL_APPEND_ELEM2(head, _tmp, add, prev, next); \
+ } else { \
+ (head) = (add); \
+ (head)->next = (head); \
+ (head)->prev = (head); \
+ } \
+} while (0)
+
+#define CDL_LOWER_BOUND(head,elt,like,cmp) \
+ CDL_LOWER_BOUND2(head,elt,like,cmp,next)
+
+#define CDL_LOWER_BOUND2(head,elt,like,cmp,next) \
+do { \
+ if ((head) == NULL || (cmp(head, like)) >= 0) { \
+ (elt) = NULL; \
+ } else { \
+ for ((elt) = (head); (elt)->next != (head); (elt) = (elt)->next) { \
+ if ((cmp((elt)->next, like)) >= 0) { \
+ break; \
+ } \
+ } \
+ } \
+} while (0)
+
+#define CDL_DELETE(head,del) \
+ CDL_DELETE2(head,del,prev,next)
+
+#define CDL_DELETE2(head,del,prev,next) \
+do { \
+ if (((head)==(del)) && ((head)->next == (head))) { \
+ (head) = NULL; \
+ } else { \
+ (del)->next->prev = (del)->prev; \
+ (del)->prev->next = (del)->next; \
+ if ((del) == (head)) (head)=(del)->next; \
+ } \
+} while (0)
+
+#define CDL_COUNT(head,el,counter) \
+ CDL_COUNT2(head,el,counter,next) \
+
+#define CDL_COUNT2(head, el, counter,next) \
+do { \
+ (counter) = 0; \
+ CDL_FOREACH2(head,el,next) { ++(counter); } \
+} while (0)
+
+#define CDL_FOREACH(head,el) \
+ CDL_FOREACH2(head,el,next)
+
+#define CDL_FOREACH2(head,el,next) \
+ for ((el)=(head);el;(el)=(((el)->next==(head)) ? NULL : (el)->next))
+
+#define CDL_FOREACH_SAFE(head,el,tmp1,tmp2) \
+ CDL_FOREACH_SAFE2(head,el,tmp1,tmp2,prev,next)
+
+#define CDL_FOREACH_SAFE2(head,el,tmp1,tmp2,prev,next) \
+ for ((el) = (head), (tmp1) = (head) ? (head)->prev : NULL; \
+ (el) && ((tmp2) = (el)->next, 1); \
+ (el) = ((el) == (tmp1) ? NULL : (tmp2)))
+
+#define CDL_SEARCH_SCALAR(head,out,field,val) \
+ CDL_SEARCH_SCALAR2(head,out,field,val,next)
+
+#define CDL_SEARCH_SCALAR2(head,out,field,val,next) \
+do { \
+ CDL_FOREACH2(head,out,next) { \
+ if ((out)->field == (val)) break; \
+ } \
+} while (0)
+
+#define CDL_SEARCH(head,out,elt,cmp) \
+ CDL_SEARCH2(head,out,elt,cmp,next)
+
+#define CDL_SEARCH2(head,out,elt,cmp,next) \
+do { \
+ CDL_FOREACH2(head,out,next) { \
+ if ((cmp(out,elt))==0) break; \
+ } \
+} while (0)
+
+#define CDL_REPLACE_ELEM2(head, el, add, prev, next) \
+do { \
+ assert((head) != NULL); \
+ assert((el) != NULL); \
+ assert((add) != NULL); \
+ if ((el)->next == (el)) { \
+ (add)->next = (add); \
+ (add)->prev = (add); \
+ (head) = (add); \
+ } else { \
+ (add)->next = (el)->next; \
+ (add)->prev = (el)->prev; \
+ (add)->next->prev = (add); \
+ (add)->prev->next = (add); \
+ if ((head) == (el)) { \
+ (head) = (add); \
+ } \
+ } \
+} while (0)
+
+#define CDL_REPLACE_ELEM(head, el, add) \
+ CDL_REPLACE_ELEM2(head, el, add, prev, next)
+
+#define CDL_PREPEND_ELEM2(head, el, add, prev, next) \
+do { \
+ if (el) { \
+ assert((head) != NULL); \
+ assert((add) != NULL); \
+ (add)->next = (el); \
+ (add)->prev = (el)->prev; \
+ (el)->prev = (add); \
+ (add)->prev->next = (add); \
+ if ((head) == (el)) { \
+ (head) = (add); \
+ } \
+ } else { \
+ CDL_APPEND2(head, add, prev, next); \
+ } \
+} while (0)
+
+#define CDL_PREPEND_ELEM(head, el, add) \
+ CDL_PREPEND_ELEM2(head, el, add, prev, next)
+
+#define CDL_APPEND_ELEM2(head, el, add, prev, next) \
+do { \
+ if (el) { \
+ assert((head) != NULL); \
+ assert((add) != NULL); \
+ (add)->next = (el)->next; \
+ (add)->prev = (el); \
+ (el)->next = (add); \
+ (add)->next->prev = (add); \
+ } else { \
+ CDL_PREPEND2(head, add, prev, next); \
+ } \
+} while (0)
+
+#define CDL_APPEND_ELEM(head, el, add) \
+ CDL_APPEND_ELEM2(head, el, add, prev, next)
+
+#ifdef NO_DECLTYPE
+/* Here are VS2008 / NO_DECLTYPE replacements for a few functions */
+
+#undef CDL_INSERT_INORDER2
+#define CDL_INSERT_INORDER2(head,add,cmp,prev,next) \
+do { \
+ if ((head) == NULL) { \
+ (add)->prev = (add); \
+ (add)->next = (add); \
+ (head) = (add); \
+ } else if ((cmp(head, add)) >= 0) { \
+ (add)->prev = (head)->prev; \
+ (add)->next = (head); \
+ (add)->prev->next = (add); \
+ (head)->prev = (add); \
+ (head) = (add); \
+ } else { \
+ char *_tmp = (char*)(head); \
+ while ((char*)(head)->next != _tmp && (cmp((head)->next, add)) < 0) { \
+ (head) = (head)->next; \
+ } \
+ (add)->prev = (head); \
+ (add)->next = (head)->next; \
+ (add)->next->prev = (add); \
+ (head)->next = (add); \
+ UTLIST_RS(head); \
+ } \
+} while (0)
+#endif /* NO_DECLTYPE */
+
+#endif /* UTLIST_H */
diff --git a/test/uthash-2.3.0/include/utringbuffer.h b/test/uthash-2.3.0/include/utringbuffer.h
new file mode 100644
index 0000000..cf48131
--- /dev/null
+++ b/test/uthash-2.3.0/include/utringbuffer.h
@@ -0,0 +1,108 @@
+/*
+Copyright (c) 2015-2021, Troy D. Hanson http://troydhanson.github.com/uthash/
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+/* a ring-buffer implementation using macros
+ */
+#ifndef UTRINGBUFFER_H
+#define UTRINGBUFFER_H
+
+#define UTRINGBUFFER_VERSION 2.3.0
+
+#include <stdlib.h>
+#include <string.h>
+#include "utarray.h" // for "UT_icd"
+
+typedef struct {
+ unsigned i; /* index of next available slot; wraps at n */
+ unsigned n; /* capacity */
+ unsigned char f; /* full */
+ UT_icd icd; /* initializer, copy and destructor functions */
+ char *d; /* n slots of size icd->sz */
+} UT_ringbuffer;
+
+#define utringbuffer_init(a, _n, _icd) do { \
+ memset(a, 0, sizeof(UT_ringbuffer)); \
+ (a)->icd = *(_icd); \
+ (a)->n = (_n); \
+ if ((a)->n) { (a)->d = (char*)malloc((a)->n * (_icd)->sz); } \
+} while(0)
+
+#define utringbuffer_clear(a) do { \
+ if ((a)->icd.dtor) { \
+ if ((a)->f) { \
+ unsigned _ut_i; \
+ for (_ut_i = 0; _ut_i < (a)->n; ++_ut_i) { \
+ (a)->icd.dtor(utringbuffer_eltptr(a, _ut_i)); \
+ } \
+ } else { \
+ unsigned _ut_i; \
+ for (_ut_i = 0; _ut_i < (a)->i; ++_ut_i) { \
+ (a)->icd.dtor(utringbuffer_eltptr(a, _ut_i)); \
+ } \
+ } \
+ } \
+ (a)->i = 0; \
+ (a)->f = 0; \
+} while(0)
+
+#define utringbuffer_done(a) do { \
+ utringbuffer_clear(a); \
+ free((a)->d); (a)->d = NULL; \
+ (a)->n = 0; \
+} while(0)
+
+#define utringbuffer_new(a,n,_icd) do { \
+ a = (UT_ringbuffer*)malloc(sizeof(UT_ringbuffer)); \
+ utringbuffer_init(a, n, _icd); \
+} while(0)
+
+#define utringbuffer_free(a) do { \
+ utringbuffer_done(a); \
+ free(a); \
+} while(0)
+
+#define utringbuffer_push_back(a,p) do { \
+ if ((a)->icd.dtor && (a)->f) { (a)->icd.dtor(_utringbuffer_internalptr(a,(a)->i)); } \
+ if ((a)->icd.copy) { (a)->icd.copy( _utringbuffer_internalptr(a,(a)->i), p); } \
+ else { memcpy(_utringbuffer_internalptr(a,(a)->i), p, (a)->icd.sz); }; \
+ if (++(a)->i == (a)->n) { (a)->i = 0; (a)->f = 1; } \
+} while(0)
+
+#define utringbuffer_len(a) ((a)->f ? (a)->n : (a)->i)
+#define utringbuffer_empty(a) ((a)->i == 0 && !(a)->f)
+#define utringbuffer_full(a) ((a)->f != 0)
+
+#define _utringbuffer_real_idx(a,j) ((a)->f ? ((j) + (a)->i) % (a)->n : (j))
+#define _utringbuffer_internalptr(a,j) ((void*)((a)->d + ((a)->icd.sz * (j))))
+#define utringbuffer_eltptr(a,j) ((0 <= (j) && (j) < utringbuffer_len(a)) ? _utringbuffer_internalptr(a,_utringbuffer_real_idx(a,j)) : NULL)
+
+#define _utringbuffer_fake_idx(a,j) ((a)->f ? ((j) + (a)->n - (a)->i) % (a)->n : (j))
+#define _utringbuffer_internalidx(a,e) (((char*)(e) >= (a)->d) ? (((char*)(e) - (a)->d)/(a)->icd.sz) : -1)
+#define utringbuffer_eltidx(a,e) _utringbuffer_fake_idx(a, _utringbuffer_internalidx(a,e))
+
+#define utringbuffer_front(a) utringbuffer_eltptr(a,0)
+#define utringbuffer_next(a,e) ((e)==NULL ? utringbuffer_front(a) : utringbuffer_eltptr(a, utringbuffer_eltidx(a,e)+1))
+#define utringbuffer_prev(a,e) ((e)==NULL ? utringbuffer_back(a) : utringbuffer_eltptr(a, utringbuffer_eltidx(a,e)-1))
+#define utringbuffer_back(a) (utringbuffer_empty(a) ? NULL : utringbuffer_eltptr(a, utringbuffer_len(a) - 1))
+
+#endif /* UTRINGBUFFER_H */
diff --git a/test/uthash-2.3.0/include/utstack.h b/test/uthash-2.3.0/include/utstack.h
new file mode 100644
index 0000000..2dbfec8
--- /dev/null
+++ b/test/uthash-2.3.0/include/utstack.h
@@ -0,0 +1,88 @@
+/*
+Copyright (c) 2018-2021, Troy D. Hanson http://troydhanson.github.com/uthash/
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifndef UTSTACK_H
+#define UTSTACK_H
+
+#define UTSTACK_VERSION 2.3.0
+
+/*
+ * This file contains macros to manipulate a singly-linked list as a stack.
+ *
+ * To use utstack, your structure must have a "next" pointer.
+ *
+ * ----------------.EXAMPLE -------------------------
+ * struct item {
+ * int id;
+ * struct item *next;
+ * }
+ *
+ * struct item *stack = NULL:
+ *
+ * int main() {
+ * int count;
+ * struct item *tmp;
+ * struct item *item = malloc(sizeof *item);
+ * item->id = 42;
+ * STACK_COUNT(stack, tmp, count); assert(count == 0);
+ * STACK_PUSH(stack, item);
+ * STACK_COUNT(stack, tmp, count); assert(count == 1);
+ * STACK_POP(stack, item);
+ * free(item);
+ * STACK_COUNT(stack, tmp, count); assert(count == 0);
+ * }
+ * --------------------------------------------------
+ */
+
+#define STACK_TOP(head) (head)
+
+#define STACK_EMPTY(head) (!(head))
+
+#define STACK_PUSH(head,add) \
+ STACK_PUSH2(head,add,next)
+
+#define STACK_PUSH2(head,add,next) \
+do { \
+ (add)->next = (head); \
+ (head) = (add); \
+} while (0)
+
+#define STACK_POP(head,result) \
+ STACK_POP2(head,result,next)
+
+#define STACK_POP2(head,result,next) \
+do { \
+ (result) = (head); \
+ (head) = (head)->next; \
+} while (0)
+
+#define STACK_COUNT(head,el,counter) \
+ STACK_COUNT2(head,el,counter,next) \
+
+#define STACK_COUNT2(head,el,counter,next) \
+do { \
+ (counter) = 0; \
+ for ((el) = (head); el; (el) = (el)->next) { ++(counter); } \
+} while (0)
+
+#endif /* UTSTACK_H */
diff --git a/test/uthash-2.3.0/include/utstring.h b/test/uthash-2.3.0/include/utstring.h
new file mode 100644
index 0000000..009b4c8
--- /dev/null
+++ b/test/uthash-2.3.0/include/utstring.h
@@ -0,0 +1,407 @@
+/*
+Copyright (c) 2008-2021, Troy D. Hanson http://troydhanson.github.com/uthash/
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+/* a dynamic string implementation using macros
+ */
+#ifndef UTSTRING_H
+#define UTSTRING_H
+
+#define UTSTRING_VERSION 2.3.0
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdarg.h>
+
+#ifdef __GNUC__
+#define UTSTRING_UNUSED __attribute__((__unused__))
+#else
+#define UTSTRING_UNUSED
+#endif
+
+#ifdef oom
+#error "The name of macro 'oom' has been changed to 'utstring_oom'. Please update your code."
+#define utstring_oom() oom()
+#endif
+
+#ifndef utstring_oom
+#define utstring_oom() exit(-1)
+#endif
+
+typedef struct {
+ char *d; /* pointer to allocated buffer */
+ size_t n; /* allocated capacity */
+ size_t i; /* index of first unused byte */
+} UT_string;
+
+#define utstring_reserve(s,amt) \
+do { \
+ if (((s)->n - (s)->i) < (size_t)(amt)) { \
+ char *utstring_tmp = (char*)realloc( \
+ (s)->d, (s)->n + (amt)); \
+ if (!utstring_tmp) { \
+ utstring_oom(); \
+ } \
+ (s)->d = utstring_tmp; \
+ (s)->n += (amt); \
+ } \
+} while(0)
+
+#define utstring_init(s) \
+do { \
+ (s)->n = 0; (s)->i = 0; (s)->d = NULL; \
+ utstring_reserve(s,100); \
+ (s)->d[0] = '\0'; \
+} while(0)
+
+#define utstring_done(s) \
+do { \
+ if ((s)->d != NULL) free((s)->d); \
+ (s)->n = 0; \
+} while(0)
+
+#define utstring_free(s) \
+do { \
+ utstring_done(s); \
+ free(s); \
+} while(0)
+
+#define utstring_new(s) \
+do { \
+ (s) = (UT_string*)malloc(sizeof(UT_string)); \
+ if (!(s)) { \
+ utstring_oom(); \
+ } \
+ utstring_init(s); \
+} while(0)
+
+#define utstring_renew(s) \
+do { \
+ if (s) { \
+ utstring_clear(s); \
+ } else { \
+ utstring_new(s); \
+ } \
+} while(0)
+
+#define utstring_clear(s) \
+do { \
+ (s)->i = 0; \
+ (s)->d[0] = '\0'; \
+} while(0)
+
+#define utstring_bincpy(s,b,l) \
+do { \
+ utstring_reserve((s),(l)+1); \
+ if (l) memcpy(&(s)->d[(s)->i], b, l); \
+ (s)->i += (l); \
+ (s)->d[(s)->i]='\0'; \
+} while(0)
+
+#define utstring_concat(dst,src) \
+do { \
+ utstring_reserve((dst),((src)->i)+1); \
+ if ((src)->i) memcpy(&(dst)->d[(dst)->i], (src)->d, (src)->i); \
+ (dst)->i += (src)->i; \
+ (dst)->d[(dst)->i]='\0'; \
+} while(0)
+
+#define utstring_len(s) ((s)->i)
+
+#define utstring_body(s) ((s)->d)
+
+UTSTRING_UNUSED static void utstring_printf_va(UT_string *s, const char *fmt, va_list ap) {
+ int n;
+ va_list cp;
+ for (;;) {
+#ifdef _WIN32
+ cp = ap;
+#else
+ va_copy(cp, ap);
+#endif
+ n = vsnprintf (&s->d[s->i], s->n-s->i, fmt, cp);
+ va_end(cp);
+
+ if ((n > -1) && ((size_t) n < (s->n-s->i))) {
+ s->i += n;
+ return;
+ }
+
+ /* Else try again with more space. */
+ if (n > -1) utstring_reserve(s,n+1); /* exact */
+ else utstring_reserve(s,(s->n)*2); /* 2x */
+ }
+}
+#ifdef __GNUC__
+/* support printf format checking (2=the format string, 3=start of varargs) */
+static void utstring_printf(UT_string *s, const char *fmt, ...)
+ __attribute__ (( format( printf, 2, 3) ));
+#endif
+UTSTRING_UNUSED static void utstring_printf(UT_string *s, const char *fmt, ...) {
+ va_list ap;
+ va_start(ap,fmt);
+ utstring_printf_va(s,fmt,ap);
+ va_end(ap);
+}
+
+/*******************************************************************************
+ * begin substring search functions *
+ ******************************************************************************/
+/* Build KMP table from left to right. */
+UTSTRING_UNUSED static void _utstring_BuildTable(
+ const char *P_Needle,
+ size_t P_NeedleLen,
+ long *P_KMP_Table)
+{
+ long i, j;
+
+ i = 0;
+ j = i - 1;
+ P_KMP_Table[i] = j;
+ while (i < (long) P_NeedleLen)
+ {
+ while ( (j > -1) && (P_Needle[i] != P_Needle[j]) )
+ {
+ j = P_KMP_Table[j];
+ }
+ i++;
+ j++;
+ if (i < (long) P_NeedleLen)
+ {
+ if (P_Needle[i] == P_Needle[j])
+ {
+ P_KMP_Table[i] = P_KMP_Table[j];
+ }
+ else
+ {
+ P_KMP_Table[i] = j;
+ }
+ }
+ else
+ {
+ P_KMP_Table[i] = j;
+ }
+ }
+
+ return;
+}
+
+
+/* Build KMP table from right to left. */
+UTSTRING_UNUSED static void _utstring_BuildTableR(
+ const char *P_Needle,
+ size_t P_NeedleLen,
+ long *P_KMP_Table)
+{
+ long i, j;
+
+ i = P_NeedleLen - 1;
+ j = i + 1;
+ P_KMP_Table[i + 1] = j;
+ while (i >= 0)
+ {
+ while ( (j < (long) P_NeedleLen) && (P_Needle[i] != P_Needle[j]) )
+ {
+ j = P_KMP_Table[j + 1];
+ }
+ i--;
+ j--;
+ if (i >= 0)
+ {
+ if (P_Needle[i] == P_Needle[j])
+ {
+ P_KMP_Table[i + 1] = P_KMP_Table[j + 1];
+ }
+ else
+ {
+ P_KMP_Table[i + 1] = j;
+ }
+ }
+ else
+ {
+ P_KMP_Table[i + 1] = j;
+ }
+ }
+
+ return;
+}
+
+
+/* Search data from left to right. ( Multiple search mode. ) */
+UTSTRING_UNUSED static long _utstring_find(
+ const char *P_Haystack,
+ size_t P_HaystackLen,
+ const char *P_Needle,
+ size_t P_NeedleLen,
+ long *P_KMP_Table)
+{
+ long i, j;
+ long V_FindPosition = -1;
+
+ /* Search from left to right. */
+ i = j = 0;
+ while ( (j < (int)P_HaystackLen) && (((P_HaystackLen - j) + i) >= P_NeedleLen) )
+ {
+ while ( (i > -1) && (P_Needle[i] != P_Haystack[j]) )
+ {
+ i = P_KMP_Table[i];
+ }
+ i++;
+ j++;
+ if (i >= (int)P_NeedleLen)
+ {
+ /* Found. */
+ V_FindPosition = j - i;
+ break;
+ }
+ }
+
+ return V_FindPosition;
+}
+
+
+/* Search data from right to left. ( Multiple search mode. ) */
+UTSTRING_UNUSED static long _utstring_findR(
+ const char *P_Haystack,
+ size_t P_HaystackLen,
+ const char *P_Needle,
+ size_t P_NeedleLen,
+ long *P_KMP_Table)
+{
+ long i, j;
+ long V_FindPosition = -1;
+
+ /* Search from right to left. */
+ j = (P_HaystackLen - 1);
+ i = (P_NeedleLen - 1);
+ while ( (j >= 0) && (j >= i) )
+ {
+ while ( (i < (int)P_NeedleLen) && (P_Needle[i] != P_Haystack[j]) )
+ {
+ i = P_KMP_Table[i + 1];
+ }
+ i--;
+ j--;
+ if (i < 0)
+ {
+ /* Found. */
+ V_FindPosition = j + 1;
+ break;
+ }
+ }
+
+ return V_FindPosition;
+}
+
+
+/* Search data from left to right. ( One time search mode. ) */
+UTSTRING_UNUSED static long utstring_find(
+ UT_string *s,
+ long P_StartPosition, /* Start from 0. -1 means last position. */
+ const char *P_Needle,
+ size_t P_NeedleLen)
+{
+ long V_StartPosition;
+ long V_HaystackLen;
+ long *V_KMP_Table;
+ long V_FindPosition = -1;
+
+ if (P_StartPosition < 0)
+ {
+ V_StartPosition = s->i + P_StartPosition;
+ }
+ else
+ {
+ V_StartPosition = P_StartPosition;
+ }
+ V_HaystackLen = s->i - V_StartPosition;
+ if ( (V_HaystackLen >= (long) P_NeedleLen) && (P_NeedleLen > 0) )
+ {
+ V_KMP_Table = (long *)malloc(sizeof(long) * (P_NeedleLen + 1));
+ if (V_KMP_Table != NULL)
+ {
+ _utstring_BuildTable(P_Needle, P_NeedleLen, V_KMP_Table);
+
+ V_FindPosition = _utstring_find(s->d + V_StartPosition,
+ V_HaystackLen,
+ P_Needle,
+ P_NeedleLen,
+ V_KMP_Table);
+ if (V_FindPosition >= 0)
+ {
+ V_FindPosition += V_StartPosition;
+ }
+
+ free(V_KMP_Table);
+ }
+ }
+
+ return V_FindPosition;
+}
+
+
+/* Search data from right to left. ( One time search mode. ) */
+UTSTRING_UNUSED static long utstring_findR(
+ UT_string *s,
+ long P_StartPosition, /* Start from 0. -1 means last position. */
+ const char *P_Needle,
+ size_t P_NeedleLen)
+{
+ long V_StartPosition;
+ long V_HaystackLen;
+ long *V_KMP_Table;
+ long V_FindPosition = -1;
+
+ if (P_StartPosition < 0)
+ {
+ V_StartPosition = s->i + P_StartPosition;
+ }
+ else
+ {
+ V_StartPosition = P_StartPosition;
+ }
+ V_HaystackLen = V_StartPosition + 1;
+ if ( (V_HaystackLen >= (long) P_NeedleLen) && (P_NeedleLen > 0) )
+ {
+ V_KMP_Table = (long *)malloc(sizeof(long) * (P_NeedleLen + 1));
+ if (V_KMP_Table != NULL)
+ {
+ _utstring_BuildTableR(P_Needle, P_NeedleLen, V_KMP_Table);
+
+ V_FindPosition = _utstring_findR(s->d,
+ V_HaystackLen,
+ P_Needle,
+ P_NeedleLen,
+ V_KMP_Table);
+
+ free(V_KMP_Table);
+ }
+ }
+
+ return V_FindPosition;
+}
+/*******************************************************************************
+ * end substring search functions *
+ ******************************************************************************/
+
+#endif /* UTSTRING_H */
diff --git a/vendor/CMakeLists.txt b/vendor/CMakeLists.txt
new file mode 100644
index 0000000..c241665
--- /dev/null
+++ b/vendor/CMakeLists.txt
@@ -0,0 +1,23 @@
+# CMakeFiles for 3rd vendor library
+
+include(ExternalProject)
+
+# GoogleTest
+ExternalProject_Add(googletest PREFIX googletest
+ URL ${CMAKE_CURRENT_SOURCE_DIR}/googletest-release-1.8.0.tar.gz
+ URL_MD5 16877098823401d1bf2ed7891d7dce36
+ CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=<INSTALL_DIR> -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE})
+
+ExternalProject_Get_Property(googletest INSTALL_DIR)
+file(MAKE_DIRECTORY ${INSTALL_DIR}/include)
+
+add_library(gtest STATIC IMPORTED GLOBAL)
+add_dependencies(gtest googletest)
+set_property(TARGET gtest PROPERTY IMPORTED_LOCATION ${INSTALL_DIR}/lib/libgtest.a)
+set_property(TARGET gtest PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${INSTALL_DIR}/include)
+set_property(TARGET gtest PROPERTY INTERFACE_LINK_LIBRARIES pthread)
+
+add_library(gmock STATIC IMPORTED GLOBAL)
+add_dependencies(gmock googletest)
+set_property(TARGET gmock PROPERTY IMPORTED_LOCATION ${INSTALL_DIR}/lib/libgmock.a)
+set_property(TARGET gmock PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${INSTALL_DIR}/include)
diff --git a/vendor/googletest-release-1.8.0.tar.gz b/vendor/googletest-release-1.8.0.tar.gz
new file mode 100644
index 0000000..a40df33
--- /dev/null
+++ b/vendor/googletest-release-1.8.0.tar.gz
Binary files differ
diff --git a/vendor/sapp-4.3.56.a47b3b5-1.el8.x86_64.rpm b/vendor/sapp-4.3.56.a47b3b5-1.el8.x86_64.rpm
new file mode 100644
index 0000000..58e3f92
--- /dev/null
+++ b/vendor/sapp-4.3.56.a47b3b5-1.el8.x86_64.rpm
Binary files differ
diff --git a/vendor/sapp-devel-4.3.56.a47b3b5-1.el8.x86_64.rpm b/vendor/sapp-devel-4.3.56.a47b3b5-1.el8.x86_64.rpm
new file mode 100644
index 0000000..9affbf6
--- /dev/null
+++ b/vendor/sapp-devel-4.3.56.a47b3b5-1.el8.x86_64.rpm
Binary files differ
diff --git a/vendor/stellar-on-sapp-2.1.1.7875675-1.el8.x86_64.rpm b/vendor/stellar-on-sapp-2.1.1.7875675-1.el8.x86_64.rpm
new file mode 100644
index 0000000..695d6bc
--- /dev/null
+++ b/vendor/stellar-on-sapp-2.1.1.7875675-1.el8.x86_64.rpm
Binary files differ