diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..ac730f2 --- /dev/null +++ b/.clang-format @@ -0,0 +1,5 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 3 +ColumnLimit: 150 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..144afb2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# top-most EditorConfig file +root = true + +[*] +end_of_line = crlf +insert_final_newline = true +trim_trailing_whitespace = true +tab_width = 3 + +[{*.sh,dockcross,docker-wine}] +end_of_line = lf + +[*.{c,h,yml}] +tab_width = 3 diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..ad04c3b --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,163 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Run FileOp CI + +on: + workflow_dispatch: # For manual triggering + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened, edited] + +defaults: + run: + shell: bash + +jobs: + + spell-check: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Install spellchecker + run: + npm install -g cspell@8.19.4 + - uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46 + id: changed-files + with: + separator: "," + - name: Run spellchecker + run: | + # Run spellchecker with changed files + mapfile -d ',' -t added_modified_files < <(printf '%s,' '${{ steps.changed-files.outputs.all_changed_and_modified_files }}') + cspell --config cspell.json --color --show-suggestions "${added_modified_files[@]}" + + container-build: + needs: + - spell-check + strategy: + fail-fast: false + matrix: + BuildType: + - Profile + - Release + runs-on: ubuntu-24.04 + container: + image: dockcross/windows-static-x64 + + steps: + - name: Install git + run: | + export DEBIAN_FRONTEND=noninteractive + apt update + apt install -y git + - uses: actions/checkout@v4 + + - name: Configure + run: | + git config --global --add safe.directory $PWD + ./scripts/cmake.configure.sh -DCMAKE_BUILD_TYPE=${{ matrix.BuildType }} + - name: Build + run: + ./scripts/cmake.build.sh + + - name: Upload ZIP + uses: actions/upload-artifact@v4 + with: + name: app-container${{ matrix.BuildType == 'Profile' && '-profile' || ''}} + path: build/FileOp.7z + + build: + needs: + - spell-check + strategy: + fail-fast: false + matrix: + BuildType: + - Profile + - Release + runs-on: Windows-latest + + steps: + - uses: actions/checkout@v4 + - name: Install ninja + run: choco install ninja + - name: Install gcovr + if: ${{ matrix.BuildType == 'Profile' }} + run: pip install git+https://github.com/gcovr/gcovr.git # gcovr==8.3 + + - name: Configure + run: ./scripts/cmake.configure.sh -DCMAKE_BUILD_TYPE=${{ matrix.BuildType }} + - name: Build + run: ./scripts/cmake.build.sh + - name: Test + run: | + ./scripts/cmake.test.sh || ExitCode=$? + echo "::group::build/Testing/Temporary/LastTest.log" + cat build/Testing/Temporary/LastTest.log + echo "::endgroup::" + exit $ExitCode + - name: Run performance test + if: always() + run: ./scripts/run_test_performance.sh 2>&1 | tee performance.txt + + - name: Create coverage report + if: ${{ matrix.BuildType == 'Profile' && always() }} + run: | + gcovr \ + --filter src/ \ + --exclude-noncode-lines build \ + --txt coverage.txt \ + --markdown coverage.md --markdown-title "Test coverage report" --markdown-file-link 'https://github.com/Spacetown/FileOp/blob/${{ github.sha }}/{file}' \ + --json coverage.json --json-pretty \ + --html-single-page --html-title "GCOVR report for $(git rev-parse HEAD)" --html-details coverage.html + cat coverage.txt + gcovr --fail-under-line 100.0 --add-tracefile coverage.json > /dev/null + - name: Upload coverage report + if: ${{ matrix.BuildType == 'Profile' && always() }} + uses: actions/upload-artifact@v4 + with: + name: coverage + path: coverage.* + + - name: Upload ZIP + if: always() + uses: actions/upload-artifact@v4 + with: + name: app-windows${{ matrix.BuildType == 'Profile' && '-profile' || ''}} + path: build/FileOp.7z + + - name: Add job summary + if: ${{ matrix.BuildType == 'Profile' && always() }} + run: | + ( + cat coverage.md + echo "" + cat performance.txt + ) >> $GITHUB_STEP_SUMMARY + + deploy: + needs: + - container-build + - build + runs-on: Windows-latest + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + pattern: app-* + # cspell:ignore oapp + - name: Test container release build + run: | + 7z x -oapp-container app-container/FileOp.7z + ./app-container/FileOp.exe --help + - name: Test windows release build + run: | + 7z x -oapp-windows app-windows/FileOp.7z + ./app-windows/FileOp.exe --help + - name: Test windows profile build + run: | + 7z x -oapp-windows-profile app-windows-profile/FileOp.7z + ./app-windows-profile/FileOp.exe --help diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..238ae03 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.venv/ +/build/ +/dockcross diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 0000000..f0ae7fd --- /dev/null +++ b/Changelog.md @@ -0,0 +1,95 @@ + +# Changelog + + + +## Unreleased + +- First release under BSD 3-Clause License. +- Use CMake and cross compiling with gcc-11. + +## 1.8.0 + +- INFDTEP-2374 + - Always use English system error messages. +- INFDTEP-2099 + - Add check for unique file names + +## 1.7.3 + +- INFDTEP-2128 + - Fix crash if tool is called with /. + +## 1.7.2 + +- INFDTEP-2097 + - Fix handling of response files bigger than buffer (0x1FFFF). + +## 1.7.1 + +- INFDTEP-2088 + - Fix option `--touch` for `copy` and `move` command. If several + files are copied or moved the target file of the operation was + always the first target file. + +## 1.7.0 + +- INFDTEP-1729 + - Add batch to tag tool. + - Fix link to pull request. +- INFDTEP-1992 + - Add support for response files. +- INFDTEP-2081 + - Add touch command. + - Add option `--touch` to `copy` and `move` commands. + - Restructure tests. + +## 1.6.1 + +- INFDTEP-1550 + - Fix crash if given command is unknown. + +## 1.6.0 + +- INFDTEP-1424 + - Remove the flush of the buffer in the cat command. + +## 1.5.0 + +- INFDTEP-1404 + - Implement option `--target-directory`. + - Internal redesign: Put each command into a single source file. + +## 1.4.0 + +- INFDTEP-1349 + - Implement `cat`, `type` and `move` command. + +- INFDTEP-1350 + - Update to new version of 000_ToolCommon. + +## 1.3.0 + +- INFDTEP-1261 + - Update help output and change documentation to markdown file. + +- INFDTEP-1262 + - Add Jenkinsfile and shell scripts to build. + +## 1.2.0 + +- INFDTEP-1140 + + - Add support for reparse points (junctions). + - The `remove` command removes the reparse point. The `copy` command copies the content of the reparse point. + +## 1.1.0 + +- INFDTEP-1077 + - Add support for wildcard in paths except `mkdir` command and the target for the `copy` command. + - Add version information to PE header of executable. + +## 1.0.0 + +- INFDTEP-1058 + - First implementation of FileOp.exe. diff --git a/FileOp.code-workspace b/FileOp.code-workspace new file mode 100644 index 0000000..6fa6d05 --- /dev/null +++ b/FileOp.code-workspace @@ -0,0 +1,47 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "actionButtons": { + "commands": [ + { + "name": "Setup env", + "command": "docker run --rm --platform linux/amd64 dockcross/windows-static-x64 > ./dockcross && chmod +x ./dockcross", + "singleInstance": true + }, + { + "name": "Configure (Profile)", + "command": "./dockcross --args '--platform linux/amd64' bash -c './scripts/cmake.configure.sh -DCMAKE_BUILD_TYPE=Profile'", + "singleInstance": true + }, + { + "name": "Configure (Release)", + "command": "./dockcross --args '--platform linux/amd64' bash -c './scripts/cmake.configure.sh -DCMAKE_BUILD_TYPE=Release'", + "singleInstance": true + }, + { + "name": "Build", + "command": "./dockcross --args '--platform linux/amd64' bash -c './scripts/cmake.build.sh'", + "singleInstance": true + } + ], + "defaultColor": "white", + "reloadButton": "↻", + "loadNpmCommands": false + }, + "editor.formatOnSave": true + }, + "extensions": { + // cspell:disable + "recommendations": [ + "seunlanlege.action-buttons", + "streetsidesoftware.code-spell-checker", + "EditorConfig.EditorConfig", + "ms-vscode.cpptools-extension-pack" + ] + // cspell:enable + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE index 6989a93..c84ecca 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ BSD 3-Clause License -Copyright (c) 2024, ZF-Group +Copyright (c) 2020-2025, ZF-Group +Copyright (c) 2025, FileOp authors Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/README.md b/README.md new file mode 100644 index 0000000..e2a687c --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ + +# FileOp + +Tool for general file operations under Windows with support of file names longer than MAX_PATH (260 characters). + +## Background + +Windows command line tools only support paths with a maximum length of 260 characters. +As workaround you need to subst the directory to a drive letter and delete the sub +tree from there. + +## Directory layout + +dir | description +--- | --- +`build` | *ignored*: Storage of build-results +`src` | Storage for source files +`tests` | Storage for test scripts + +## Development + +For development a workspace for `Visual Studio Code` is configured together with a cross compiler +running under docker. + +### Build + +The project uses CMake and ninja for building the executable. The CMake configuration step is executed by +calling [scripts/cmake.configure.sh](./scripts/cmake.configure.sh) and the build by calling +[scripts/cmake.build.sh](./scripts/cmake.build.sh). In the status bar of the IDE there are buttons to +execute the tools. + +### Test + +To test the generated artifacts call `.\tools\test.cmd` or use `Terminal`->`Run Task...`->`Test` in the IDE. diff --git a/cspell.json b/cspell.json new file mode 100644 index 0000000..3ac0ba2 --- /dev/null +++ b/cspell.json @@ -0,0 +1,52 @@ +{ + "version": "0.2", + "files": [ + "/**" + ], + "ignorePaths": [ + "/.git/", + "src/mingw-unicode.c" + ], + "dictionaryDefinitions": [], + "dictionaries": [], + "enableGlobDot": true, + "words": [ + "choco", + "devcontainers", + "dockcross", + "endgroup", + "fileop", + "gcov", + "gcovr", + "mapfile", + "mklink", + "msys", + "noncode", + "popd", + "pushd", + "STREQUAL", + "venv", + "windres" + ], + "ignoreWords": [ + "WINXP", + "endforeach", + "endfunction", + "noninteractive", + "operationcopy", + "seunlanlege" + ], + "ignoreRegExpList": [ + // GH actions + "uses: [^\\s]+/[^\\s]+@[^\\s]+", + // Options + "--[a-z0-9-]+", + // CMAKE and GCC defines + "-DCMAKE[A-Z_]*", + "-DN?DEBUG", + // Functions + "tcs[a-z]+", + "[a-z]+printf" + ], + "import": [] +} \ No newline at end of file diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 0000000..8c539f0 --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1,2 @@ +# Temporary log files +/*.log \ No newline at end of file diff --git a/scripts/cmake.build.sh b/scripts/cmake.build.sh new file mode 100755 index 0000000..345205a --- /dev/null +++ b/scripts/cmake.build.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -e +THIS_DIRECTORY="$(cd "$(dirname "${BASH_SOURCE[0]}" )" && pwd)" + +set -x +pushd $THIS_DIRECTORY/../build +ninja --verbose +popd && exit $? diff --git a/scripts/cmake.configure.sh b/scripts/cmake.configure.sh new file mode 100755 index 0000000..dafb163 --- /dev/null +++ b/scripts/cmake.configure.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -e +THIS_DIRECTORY="$(cd "$(dirname "${BASH_SOURCE[0]}" )" && pwd)" + +set -x +cmake --fresh -G Ninja "$@" -S $THIS_DIRECTORY/../src -B $THIS_DIRECTORY/../build diff --git a/scripts/cmake.test.sh b/scripts/cmake.test.sh new file mode 100755 index 0000000..ee85798 --- /dev/null +++ b/scripts/cmake.test.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -e +THIS_DIRECTORY="$(cd "$(dirname "${BASH_SOURCE[0]}" )" && pwd)" + +pushd $THIS_DIRECTORY/../build +ninja --verbose test "$@" +popd && exit $? diff --git a/scripts/get_pe_header.rc.sh b/scripts/get_pe_header.rc.sh new file mode 100755 index 0000000..4c9f4a2 --- /dev/null +++ b/scripts/get_pe_header.rc.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -e +THIS_DIRECTORY="$(cd "$(dirname "${BASH_SOURCE[0]}" )" && pwd)" + +CurrentDate=$(date) +CurrentYear=$(date --date="${CurrentDate}" +%Y) +FileVersion=$(date --date="${CurrentDate}" +%Y,%-m,%-d,%H%M) +FileVersionInfoString=$(date --date="${CurrentDate}" +%Y.%m.%d.%H%M) +GitCommitSha=$(git rev-parse HEAD) +GitRemoteUrl=$(git remote get-url origin) +ReleaseTag=$(${THIS_DIRECTORY}/get_version.sh) + +# cspell:disable +echo """ +#include + +VS_VERSION_INFO VERSIONINFO +FILEVERSION ${FileVersion} +PRODUCTVERSION $(echo "${ReleaseTag}" | sed -e "s/\./,/g"),0 +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +FILEFLAGS VS_FF_DEBUG +FILEOS VOS_NT_WINDOWS32 +FILETYPE VFT_APP +FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK \"StringFileInfo\" + BEGIN + BLOCK \"040904E4\" + BEGIN + VALUE \"CompanyName\", \"FileOp authors\" + VALUE \"FileDescription\", \"Program for basic file operations\" + VALUE \"FileVersion\", \"${FileVersionInfoString}\" + VALUE \"InternalName\", \"$(echo ${GitRemoteUrl} | sed -e 's|\.git|/tree/|')${GitCommitSha}\" + VALUE \"LegalCopyright\", \"FileOp authors, 2020-${CurrentYear}\" + VALUE \"OriginalFilename\", \"FileOp.exe\" + VALUE \"ProductVersion\", \"${ReleaseTag}.0\" + END + END + + BLOCK \"VarFileInfo\" + BEGIN + /* The following line should only be modified for localized versions. */ + /* It consists of any number of WORD,WORD pairs, with each pair */ + /* describing a language,codepage combination supported by the file. */ + /* */ + /* For example, a file might have values "0x409,1252" indicating that it */ + /* supports English language (0x409) in the Windows ANSI codepage (1252). */ + VALUE \"Translation\", 0x409, 1252 + END +END +""" +# cspell:enable diff --git a/scripts/get_readme.md.sh b/scripts/get_readme.md.sh new file mode 100755 index 0000000..dca41a4 --- /dev/null +++ b/scripts/get_readme.md.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -e +THIS_DIRECTORY="$(cd "$(dirname "${BASH_SOURCE[0]}" )" && pwd)" + +GitCommitSha=$(git rev-parse HEAD) +GitRemoteUrl=$(git remote get-url origin | sed -e "s|\.git|/tree/${GitCommitSha}|") + +cat ${THIS_DIRECTORY}/../README.md +echo """ + +## Build infos + +Build from commit [${GitCommitSha}](${GitRemoteUrl}) +""" diff --git a/scripts/get_version.sh b/scripts/get_version.sh new file mode 100755 index 0000000..3a058cb --- /dev/null +++ b/scripts/get_version.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e + +THIS_DIRECTORY="$(cd "$(dirname "${BASH_SOURCE[0]}" )" && pwd)" + +CurrentTag="$(sed --silent -e '/^## / { s/^## //; p }' $THIS_DIRECTORY/../Changelog.md \ + | head --lines 1 \ + | sed --silent -e '/^[0-9]*\.[0-9]*\.[0-9]*/ p')" +if [ -z "${CurrentTag}" ] ; then + CurrentTag="0.0.0" +fi +echo "${CurrentTag}" \ No newline at end of file diff --git a/scripts/run_test_performance.sh b/scripts/run_test_performance.sh new file mode 100755 index 0000000..6257b5a --- /dev/null +++ b/scripts/run_test_performance.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +set -e +set -o pipefail +THIS_DIRECTORY="$(cd "$(dirname "${BASH_SOURCE[0]}" )" && pwd)" + +export PATH=$THIS_DIRECTORY/../build:$PATH + +max=100 + +rm -rf /tmp/$$ +mkdir /tmp/$$ +pushd /tmp/$$ > /dev/null + +function append_rsp() { + local current_dir=$1; shift + local index=$1; shift + + if [ $index -le $max ] ; then + echo "$current_dir " >> mkdir.rsp + for i in $(seq 1 $max); do + echo "$current_dir/f$i " >> touch.rsp + done + append_rsp $current_dir/$index "$(($index + 1))" + fi +} + +echo """## Performance test + +Creating lists for $max directories with with a depth of $max levels and $max files in each""" +time append_rsp root 0 +echo """Lists contain $(wc --lines < mkdir.rsp) directories and $(wc --lines < touch.rsp) files. +Longest filename has $(wc --max-line-length < touch.rsp) characters. +""" +sed --regexp-extended -s 's|/|\\\\|g' < mkdir.rsp > mkdir.backslash.rsp +sed --regexp-extended -s 's|/|\\\\|g' < touch.rsp > touch.backslash.rsp + +############################################################################### +echo """ +### Using cmd + +#### Creating directories""" +time xargs --max-chars=8000 --arg-file mkdir.backslash.rsp -I '{}' cmd.exe //c "mkdir {}" \; +echo " +#### Creating files" +time xargs --max-chars=8000 --arg-file touch.backslash.rsp -I '{}' cmd.exe //c "for %f in ( {} ) do @( echo. > %f )" \; +echo " +#### Removing tree" +time rm -rf root + +############################################################################### +echo """ +### Using bash + +#### Creating directories""" +time xargs --max-chars=8000 --arg-file mkdir.rsp mkdir +echo " +#### Creating files" +time xargs --max-chars=8000 --arg-file touch.rsp touch +echo " +#### Removing tree" +time rm -rf root + +############################################################################### +echo """ +### Using FileOp + +#### Creating directories""" +time xargs --max-chars=8000 --arg-file mkdir.rsp FileOp.exe mkdir +echo " +#### Creating files" +time xargs --max-chars=8000 --arg-file touch.rsp FileOp.exe touch +echo " +#### Removing tree" +time FileOp.exe remove --recursive --force root + +############################################################################### +echo """ +### Using FileOp with rsp + +#### Creating directories""" +time FileOp.exe mkdir @mkdir.rsp +echo " +#### Creating files" +time FileOp.exe touch @touch.rsp +echo " +#### Removing tree" +time FileOp.exe remove --recursive --force root diff --git a/src/BasicFileOp.c b/src/BasicFileOp.c new file mode 100644 index 0000000..54b96af --- /dev/null +++ b/src/BasicFileOp.c @@ -0,0 +1,586 @@ + +#include "BasicFileOp.h" +#include "FileOp.h" +#include "Message.h" + +#include +#include +#include +#include +#include +#include + +#include + +//! The prefix for a DOS device path +LPCTSTR DosDevicePathPrefix = _T("\\\\?\\"); +// The length of the DOS device path prefix +const int DOS_DEVICE_PREFIX_LENGTH = sizeof(DosDevicePathPrefix) / sizeof(TCHAR); +//! The prefix for a DOS device path prefix for UNC paths +LPCTSTR DosDevicePathPrefixUnc = _T("\\\\?\\UNC\\"); +// The length of the DOS device path prefix for UNC paths +const int DOS_DEVICE_PREFIX_UNC_LENGTH = sizeof(DosDevicePathPrefixUnc) / sizeof(TCHAR); + +//! The buffer for the source path if needed. +TCHAR DosDevicePath[DOS_DEVICE_BUFFER_SIZE] = _T(""); +//! The buffer for the target path. +TCHAR TargetDosDevicePath[DOS_DEVICE_BUFFER_SIZE] = _T(""); +//! The buffer for the current response file. +TCHAR DosDevicePathResponseFile[DOS_DEVICE_BUFFER_SIZE] = _T(""); + +TCHAR **KnownFilenames = NULL; + +//! Size ot the read buffer. +#define SIZE_IO_BUFFER (0x1FFFF) +//! The buffer to read a file. +char IoBuffer[SIZE_IO_BUFFER]; +//! The buffer to read a file. +TCHAR IoBufferWideCharacter[SIZE_IO_BUFFER]; + +//! The time format for touch +static const TCHAR TIME_FORMAT[] = _T("%4d-%02d-%02dT%02d:%02d:%02d"); +static LPFILETIME ptr_file_time = NULL; + +/*! + * Create the used DOS device path (starting with \\?\). Path is normalized and + * changed to an absolute path.. + * + * @param currentSourcePath The path to normalize. + * @param currentPath The target buffer for the DOS device path. + */ +void createDosDevicePath(LPCTSTR currentSourcePath, LPTSTR currentPath) { + static TCHAR TempSourcePath[DOS_DEVICE_BUFFER_SIZE]; + + DWORD Size = GetFullPathName(currentSourcePath, DOS_DEVICE_BUFFER_SIZE, TempSourcePath, NULL); + // GCOVR_EXCL_START + if (Size == 0L) { + printLastError(_T("Could not get full name of %s"), currentSourcePath); + flushOutputAndExit(EXIT_FAILURE); + } else if (Size > DOS_DEVICE_BUFFER_SIZE) { + printLastError(_T("Could not get full name of %s. Buffer is to small, needed are %d characters."), currentSourcePath, Size); + flushOutputAndExit(EXIT_FAILURE); + } + // GCOVR_EXCL_STOP + LPTSTR StartOfPath = TempSourcePath; + // Create the used DOS device path (starting with \\?\). + if (_tcsncmp(StartOfPath, DosDevicePathPrefix, DOS_DEVICE_PREFIX_LENGTH) == 0) { + currentPath[0] = _T('\0'); + } else { + if (_tcsncmp(StartOfPath, _T("\\\\"), 2) == 0) { + StartOfPath += 2; + _tcscpy(currentPath, DosDevicePathPrefixUnc); + // \\?\UNC\server\share + } else { + _tcscpy(currentPath, DosDevicePathPrefix); + } + } + + _tcscat(currentPath, StartOfPath); + + return; +} + +/*! + * Get a readable file without DOS device prefix. + * + * @param currentPath The path to get the prefix from + * @return The path after the DOS device prefix + */ +LPCTSTR getReadableFilename(LPCTSTR currentPath) { return ¤tPath[DOS_DEVICE_PREFIX_LENGTH]; } + +/*! + * Check if the attributes are valid + * + * @param dwAttrs The attributes to check. + * @return eOk if file attributes are valid + */ +tResult isValidFileAttributes(const DWORD dwAttrs) { return (dwAttrs == INVALID_FILE_ATTRIBUTES) ? eError : eOk; } + +/*! + * Check if readonly attribute is set. + * + * @param dwAttrs The attributes to check. + * @return eOk if readonly attribute is set, else eError. + */ +tResult isReadonly(const DWORD dwAttrs) { return (isValidFileAttributes(dwAttrs) && ((dwAttrs & FILE_ATTRIBUTE_READONLY) != 0)) ? eOk : eError; } + +/*! + * Check if it is a reparse point + * + * @param dwAttrs The attributes to check. + * @return eOk if element is a reparse point, else eError. + */ +tResult isReparsePoint(const DWORD dwAttrs) { + return (isValidFileAttributes(dwAttrs) && ((dwAttrs & FILE_ATTRIBUTE_REPARSE_POINT) != 0)) ? eOk : eError; +} + +/*! + * Check if directory attribute is set. + * + * @param dwAttrs The attributes to check. + * @return eOk if directory attribute is set, else eError. + */ +tResult isDirectory(const DWORD dwAttrs) { return (isValidFileAttributes(dwAttrs) && ((dwAttrs & FILE_ATTRIBUTE_DIRECTORY) != 0)) ? eOk : eError; } + +/*! + * Clear the readonly attribute. + * + * @param currentPath The file for which the attribute are changed. + * @param dwAttrs The current attribute value. + * @return eOk on success, else eError. + */ +tResult clearReadonly(LPCTSTR currentPath, const DWORD dwAttrs) { + if (Debug) { + printOut(_T("Clear readonly flag of element %s\n"), getReadableFilename(currentPath)); + } + if (SetFileAttributes(currentPath, dwAttrs & (~FILE_ATTRIBUTE_READONLY)) == 0) { + // GCOVR_EXCL_START + return printLastError(_T("Can't clear readonly flag of element %s"), getReadableFilename(currentPath)); + // GCOVR_EXCL_STOP + } + return eOk; +} + +/*! + * Create a single directory. + * + * @param currentPath The directory to create. + * @return eOk on success, else eError. + */ +tResult createSingleDirectory(LPCTSTR currentPath) { + if (Debug) { + printOut(_T("Create directory %s\n"), getReadableFilename(currentPath)); + } + if (CreateDirectory(currentPath, NULL) == 0) { + return printLastError(_T("Can't create directory %s"), getReadableFilename(currentPath)); + } + + return eOk; +} + +/*! + * Delete a empty directory directory. + * + * @param currentPath The directory to remove. + * @return eOk on success, else eError. + */ +tResult removeEmptyDirectory(LPCTSTR currentPath) { + if (Debug) { + printOut(_T("Remove directory %s\n"), getReadableFilename(currentPath)); + } + if (RemoveDirectory(currentPath) == 0) { + return printLastError(_T("Can't remove directory %s"), getReadableFilename(currentPath)); + } + + return eOk; +} + +/*! + * Delete a reparse point. + * + * @param currentPath The reparse point to remove. + * @return eOk on success, else eError. + */ +tResult removeReparsePoint(LPCTSTR currentPath) { + if (Debug) { + printOut(_T("Remove reparse point %s\n"), getReadableFilename(currentPath)); + } + + HANDLE Handle = + CreateFile(currentPath, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL); + if (Handle == INVALID_HANDLE_VALUE) { + return printLastError(_T("Can't get handle to %s"), getReadableFilename(currentPath)); // GCOVR_EXCL_LINE + } + + REPARSE_DATA_BUFFER ReparseDataBuffer = {0}; + ReparseDataBuffer.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT; + + DWORD BytesReturned = 0; + if (DeviceIoControl(Handle, FSCTL_DELETE_REPARSE_POINT, &ReparseDataBuffer, REPARSE_DATA_BUFFER_HEADER_SIZE, NULL, 0, &BytesReturned, 0) == 0) { + // GCOVR_EXCL_START + printLastError(_T("Can't remove reparse point %s"), getReadableFilename(currentPath)); + if (CloseHandle(Handle) == 0) { + printLastError(_T("Can't close handle to %s"), getReadableFilename(currentPath)); + } + return eError; + // GCOVR_EXCL_STOP + } + if (CloseHandle(Handle) == 0) { + return printLastError(_T("Can't close handle to %s"), getReadableFilename(currentPath)); // GCOVR_EXCL_LINE + } + return removeEmptyDirectory(currentPath); +} + +/*! + * Delete a single file. + * + * @param currentPath The file to remove. + * @return eOk on success, else eError. + */ +tResult removeSingleFile(LPCTSTR currentPath) { + if (Debug) { + printOut(_T("Remove file %s\n"), getReadableFilename(currentPath)); + } + if (DeleteFile(currentPath) == 0) { + return printLastError(_T("Can't remove file %s"), getReadableFilename(currentPath)); + } + + return eOk; +} + +/*! + * Copy a single file. + * + * @param currentSourcePath The file to copy. + * @param currentTargetPath The target file. + * @param force Overwrite existing file. + * + * @return eOk on success, else eError. + */ +tResult copySingleFile(LPCTSTR currentSourcePath, LPCTSTR currentTargetPath, tBool force) { + if (Debug) { + printOut(_T("Copy file %s to %s\n"), getReadableFilename(currentSourcePath), getReadableFilename(currentTargetPath)); + } + if (CopyFile(currentSourcePath, currentTargetPath, force ? 0 : 1) == 0) { + return printLastError(_T("Can't copy file %s to %s"), getReadableFilename(currentSourcePath), getReadableFilename(currentTargetPath)); + } + + return eOk; +} + +/*! + * Move a single file. + * + * @param currentSourcePath The file to copy. + * @param currentTargetPath The target file. + * @param force Overwrite existing file. + * + * @return eOk on success, else eError. + */ +tResult moveSingleFile(LPCTSTR currentSourcePath, LPCTSTR currentTargetPath, tBool force) { + if (Debug) { + printOut(_T("Move file %s to %s\n"), getReadableFilename(currentSourcePath), getReadableFilename(currentTargetPath)); + } + if (MoveFileEx(currentSourcePath, currentTargetPath, MOVEFILE_COPY_ALLOWED | (force ? MOVEFILE_REPLACE_EXISTING : 0)) == 0) { + return printLastError(_T("Can't move file %s to %s"), getReadableFilename(currentSourcePath), getReadableFilename(currentTargetPath)); + } + + return eOk; +} + +/*! + * Touch a file. + * + * @param currentPath The file to touch. + * @param localTime The time to use, NULL to use current time. + * @param createIfMissing If eOk, the file is created if it doesn't exist. + * + * @return eOk if the file was successfully touched, else eError. + */ +tResult touchSingleFile(LPCTSTR currentPath, tBool createIfMissing) { + HANDLE hFile; + + if (Debug) { + printOut(_T("Touch file %s\n"), getReadableFilename(currentPath)); + } + hFile = CreateFile(currentPath, FILE_WRITE_ATTRIBUTES, 0, NULL, createIfMissing ? OPEN_ALWAYS : OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (hFile == INVALID_HANDLE_VALUE) { + return printLastError(_T("Can't get handle to %s"), getReadableFilename(currentPath)); // GCOVR_EXCL_LINE + } else { + LPFILETIME pft = ptr_file_time; + // Use current system time if not defined in command line + if (pft == NULL) { + static FILETIME ft; + static SYSTEMTIME st; + pft = &ft; + GetSystemTime(&st); + // Converts the current system time to file time format + if (SystemTimeToFileTime(&st, &ft) == 0) { + return printLastError(_T("Can't convert system time to file time")); // GCOVR_EXCL_LINE + } + } + if (SetFileTime(hFile, (LPFILETIME)NULL, pft, pft) == 0) { + return printLastError(_T("Can't set access and write time of %s"), getReadableFilename(currentPath)); // GCOVR_EXCL_LINE + } + if (CloseHandle(hFile) == 0) { + return printLastError(_T("Can't close handle to %s"), getReadableFilename(currentPath)); // GCOVR_EXCL_LINE + } + } + + return eOk; +} + +/*! + * Open the given path for reading. + * + * @param currentPath The file to open. + * @param handle A pointer to write the opened handle to. + * + * @return eOk if the file was opened successfully, also setting argument + * handle, else eError. + */ +tResult openFile(LPCTSTR currentPath, HANDLE *handle) { + IoBuffer[0] = '\0'; + + *handle = CreateFile(currentPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (*handle == INVALID_HANDLE_VALUE) { + // GCOVR_EXCL_START + return printLastError(_T("Can't get handle to %s"), getReadableFilename(currentPath)); + // GCOVR_EXCL_STOP + } + + return eOk; +} + +/*! + * Close the handle to the file. + * + * @param currentPath The file to close (needed for error message). + * @param handle The handle to close. + * + * @return The handle or INVALID_HANDLE_VALUE if open wasn't successful. + */ +tResult closeFile(LPCTSTR currentPath, HANDLE *handle) { + if (CloseHandle(handle) == 0) { + return printLastError(_T("Can't close handle to %s"), getReadableFilename(currentPath)); // GCOVR_EXCL_LINE + } + + return eOk; +} + +/*! + * Check if the option is --time. + * + * @param argv The current argv pointer. + * + * @return eTrue if --time option else, else eError + */ +tBool isTimeOption(wchar_t *argv) { return _tcsncmp(argv, _T("--time"), 6) == 0; } + +/*! + * Check if the option is --time. + * + * @param argv The current argv pointer. + * + * @return eOk if the file was opened successfully, also setting argument + * handle, else eError. + */ +tResult storeTimeValue(int *argc, wchar_t **argv[]) { + LPCTSTR localTime = NULL; + if (_tcsncmp(**argv, _T("--time="), 7) == 0) { + localTime = &(**argv)[7]; + } else { + if (*argc <= 1) { + return printErr(_T("Option %s needs an argument.\n"), **argv); + } + localTime = *(++*argv); + --*argc; + } + + if (Debug) { + printOut(_T("Converting time string '%s'\n"), localTime); + } + + SYSTEMTIME lt, st; + int count = swscanf(localTime, TIME_FORMAT, <.wYear, <.wMonth, <.wDay, <.wHour, <.wMinute, <.wSecond); + if (count < 3) { + return printErr(_T("Wrong format for time %s, expected yyyy-mm-dd[Thh:mm[:ss]].\n"), localTime); + } + if (count == 3) { + lt.wHour = 12; + lt.wMinute = 0; + lt.wSecond = 0; + } else if (count == 5) { + lt.wSecond = 0; + } else if (count != 6) { + return printErr(_T("Wrong format for time %s, expected yyyy-mm-dd[Thh:mm[:ss]].\n"), localTime); + } + lt.wMilliseconds = 0; + if (TzSpecificLocalTimeToSystemTime((TIME_ZONE_INFORMATION *)NULL, <, &st) == 0) { + return printLastError(_T("Can't convert local time %s to system time"), localTime); // GCOVR_EXCL_LINE + } + + static FILETIME ft; + // Converts the current system time to file time format + if (SystemTimeToFileTime(&st, &ft) == 0) { + return printLastError(_T("Can't convert system time to file time")); // GCOVR_EXCL_LINE + } + + ptr_file_time = &ft; + + return eOk; +} + +/*! + * Run a command for each argument. If the argument starts with a @ the + * file is opened and read line by line, running the given command for each + * line. + * + * @param argc The number of arguments. + * @param argv The arguments. + * @param command The pointer to the function which shall be executed for each + * file. + * + * @return eOk on success, else eError + */ +tResult runCommandForEachInputLine(int argc, wchar_t *argv[], tResult (*command)(void)) { + tResult result = eOk; + while ((result == eOk) && (argc-- != 0)) { + TCHAR *filename = *(argv++); + if (filename[0] == _T('@')) { + createDosDevicePath(&filename[1], DosDevicePathResponseFile); + HANDLE InHandle; + result &= openFile(DosDevicePathResponseFile, &InHandle); + if (result == eOk) { + DWORD BytesRead = 0; + *IoBuffer = '\0'; + *IoBufferWideCharacter = _T('\0'); + do { + // Start behind the current content + DWORD Offset = _tcslen(IoBufferWideCharacter); + if (FALSE == ReadFile(InHandle, IoBuffer, SIZE_IO_BUFFER - Offset - 1, &BytesRead, NULL)) { + result &= printLastError(_T("Error reading file %s"), DosDevicePathResponseFile); // GCOVR_EXCL_LINE + } + if (BytesRead != 0) { + // Add a \0 after the read content + IoBuffer[BytesRead] = '\0'; + // ...and convert it. + if (MultiByteToWideChar(CP_ACP, 0, IoBuffer, -1, &IoBufferWideCharacter[Offset], SIZE_IO_BUFFER - Offset) == 0) { + result &= printLastError(_T("Error converting file content of %s"), DosDevicePathResponseFile); // GCOVR_EXCL_LINE + } else { + LPTSTR PtrStart = IoBufferWideCharacter; + LPTSTR PtrEnd = &IoBufferWideCharacter[BytesRead + Offset]; + do { + LPTSTR PtrEndOfLine = _tcschr(PtrStart, _T('\n')); + if (PtrEndOfLine != NULL) { + LPTSTR PtrStartNextLine = &PtrEndOfLine[1]; + *PtrEndOfLine = _T('\0'); + do { + --PtrEndOfLine; + if ((*PtrEndOfLine == _T('\r')) || (*PtrEndOfLine == _T(' '))) { + *PtrEndOfLine = _T('\0'); + } + } while ((*PtrEndOfLine == _T('\0')) && (PtrEndOfLine > PtrStart)); + // If line isn't empty + if (PtrEndOfLine > PtrStart) { + createDosDevicePath(PtrStart, DosDevicePath); + result &= command(); + } + // Clear this line (needed if last char in buffer is a + // linebreak + *PtrStart = _T('\0'); + PtrStart = PtrStartNextLine; + } else { + // Move the rest of the string to the start of the buffer + DWORD Index = _tcslen(PtrStart) + 1; + for (; Index > 0; --Index) { + IoBufferWideCharacter[Index] = PtrStart[Index]; + } + IoBufferWideCharacter[0] = PtrStart[0]; + PtrStart = PtrEnd; + } + } while ((result == eOk) && (PtrStart < PtrEnd)); + } + } + } while ((result == eOk) && (BytesRead != 0)); + result &= closeFile(DosDevicePathResponseFile, InHandle); + } + } else { + createDosDevicePath(filename, DosDevicePath); + result &= command(); + } + } + + return result; +} + +/*! + * Check if the last part of the input is unique. + * + * @return eOk if everything is unique, else eError + */ +tResult checkUniqueName(void) { + tResult result = eOk; + + // Get the filename and make it lowercase + LPTSTR PtrFilenameStart = _tcsrchr(DosDevicePath, _T('\\')) + 1; + (void)_tcslwr(PtrFilenameStart); + TCHAR **CurrentFilename = KnownFilenames; + while (*CurrentFilename != NULL) { + if (_tcscmp(PtrFilenameStart, *CurrentFilename) == 0) { + result &= printErr(_T("File in source list will overwrite each other: %s"), PtrFilenameStart); + break; + } + ++CurrentFilename; + } + if (result == eOk) { + *CurrentFilename = malloc(_tcslen(PtrFilenameStart) * sizeof(TCHAR *)); + if (*CurrentFilename == NULL) { + result &= printLastError(_T("Can't allocate memory for current filenames")); // GCOVR_EXCL_LINE + } + _tcscpy(*CurrentFilename, PtrFilenameStart); + } + + return result; +} + +tResult checkUniqueNames(int argc, wchar_t *argv[]) { + tResult result = eOk; + size_t ListSize = (argc + 1) * sizeof(TCHAR *); // +1 to have a trailing NULL pointer + // (needed for freeing the space) + + KnownFilenames = malloc(ListSize); + if (KnownFilenames == NULL) { + result &= printLastError(_T("Can't allocate memory for list of filenames")); // GCOVR_EXCL_LINE + } else { + memset(KnownFilenames, 0, ListSize); + + result &= runCommandForEachInputLine(argc, argv, checkUniqueName); + + TCHAR **CurrentFilename = KnownFilenames; + while (*CurrentFilename != NULL) { + free(*CurrentFilename); + if (errno != 0) { + result &= printLastError(_T("Can't free memory of filename")); // GCOVR_EXCL_LINE + } + ++CurrentFilename; + } + free(KnownFilenames); + if (errno != 0) { + result &= printLastError(_T("Can't free memory of list of filenames")); // GCOVR_EXCL_LINE + } + } + + return result; +} + +/*! + * Print a file to a given handle. + * + * @param outHandle The handle to which the file is printed. + * + * @return eOk on success, else eError. + */ +tResult printFileToHandle(LPCTSTR currentPath, HANDLE *outHandle) { + tResult result = eOk; + + HANDLE InHandle; + result &= openFile(currentPath, &InHandle); + if (result == eOk) { + DWORD BytesRead = 0; + do { + if (FALSE == ReadFile(InHandle, IoBuffer, SIZE_IO_BUFFER, &BytesRead, NULL)) { + result &= printLastError(_T("Error reading file %s"), getReadableFilename(currentPath)); // GCOVR_EXCL_LINE + } + if (BytesRead != 0) { + DWORD BytesWritten = 0; + if (WriteFile(outHandle, IoBuffer, BytesRead, &BytesWritten, NULL) == 0) { + result &= printLastError(_T("Error writing to output handle")); // GCOVR_EXCL_LINE + } + } + } while ((result == eOk) && (BytesRead != 0)); + result &= closeFile(currentPath, InHandle); + } + + return result; +} diff --git a/src/BasicFileOp.h b/src/BasicFileOp.h new file mode 100644 index 0000000..13e28a8 --- /dev/null +++ b/src/BasicFileOp.h @@ -0,0 +1,35 @@ +#ifndef BASICFILEOP_INCLUDED +#define BASICFILEOP_INCLUDED + +#include "Types.h" + +//! Size of the buffer for the DOS device path. +#define DOS_DEVICE_BUFFER_SIZE (0xFFFF) +//! The buffer for the source path if needed. +extern TCHAR DosDevicePath[DOS_DEVICE_BUFFER_SIZE]; +//! The buffer for the target path. +extern TCHAR TargetDosDevicePath[DOS_DEVICE_BUFFER_SIZE]; + +extern void createDosDevicePath(LPCTSTR currentSourcePath, LPTSTR currentPath); +extern LPCTSTR getReadableFilename(LPCTSTR currentPath); + +extern tResult isValidFileAttributes(const DWORD dwAttrs); +extern tResult isReadonly(const DWORD dwAttrs); +extern tResult isReparsePoint(const DWORD dwAttrs); +extern tResult isDirectory(const DWORD dwAttrs); +extern tResult clearReadonly(LPCTSTR currentPath, const DWORD dwAttrs); +extern tResult createSingleDirectory(LPCTSTR currentPath); +extern tResult removeEmptyDirectory(LPCTSTR currentPath); +extern tResult removeReparsePoint(LPCTSTR currentPath); +extern tResult removeSingleFile(LPCTSTR currentPath); +extern tResult copySingleFile(LPCTSTR currentSourcePath, LPCTSTR currentTargetPath, tBool force); +extern tResult moveSingleFile(LPCTSTR currentSourcePath, LPCTSTR currentTargetPath, tBool force); +extern tResult touchSingleFile(LPCTSTR currentPath, tBool createIfMissing); +extern tResult printFileToHandle(LPCTSTR currentPath, HANDLE *handle); + +extern tBool isTimeOption(wchar_t *argv); +extern tResult storeTimeValue(int *argc, wchar_t **argv[]); + +extern tResult runCommandForEachInputLine(int argc, wchar_t *argv[], tResult (*command)(void)); +extern tResult checkUniqueNames(int argc, wchar_t *argv[]); +#endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..143dedb --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,278 @@ +cmake_minimum_required(VERSION 3.25.1) + +string(APPEND CMAKE_C_FLAGS_PROFILE "--coverage") +string(APPEND CMAKE_RC_FLAGS "--verbose") +string(APPEND CMAKE_EXE_LINKER_FLAGS "-static") + +project(FileOp C RC) + + add_custom_target( + resource_file + COMMAND bash -c ${CMAKE_CURRENT_LIST_DIR}/../scripts/get_pe_header.rc.sh > PeHeader.rc + BYPRODUCTS PeHeader.rc + ) + + add_executable( + FileOp + PeHeader.rc + BasicFileOp.c + FileOp.c + Message.c + OperationCat.c + OperationCopy.c + OperationMkdir.c + OperationMove.c + OperationRemove.c + OperationTouch.c + ) + + add_custom_target( + readme ALL + COMMAND bash -c ${CMAKE_CURRENT_LIST_DIR}/../scripts/get_readme.md.sh > README.md + BYPRODUCTS README.md + ) + + add_custom_target( + create_zip ALL + COMMAND ${CMAKE_COMMAND} -E tar "cfv" "FileOp.7z" --format=7zip + FileOp.exe + README.md + DEPENDS FileOp readme + BYPRODUCTS FileOp.7z + ) + + include(CTest) + + function(test_fileop) + set(options WILL_FAIL) + set(oneValueArgs NAME WORKING_DIRECTORY FILE_TIME_STAMP) + set(multiValueArgs ARGS FAIL_REGULAR_EXPRESSION PASS_REGULAR_EXPRESSION) + cmake_parse_arguments( + PARSE_ARGV 0 arg + "${options}" "${oneValueArgs}" "${multiValueArgs}" + ) + + add_test( + NAME ${arg_NAME} + COMMAND FileOp.exe ${arg_ARGS} + WORKING_DIRECTORY ${arg_WORKING_DIRECTORY} + ) + set_property( + TEST ${arg_NAME} + PROPERTY WILL_FAIL ${arg_WILL_FAIL} + ) + if (arg_FAIL_REGULAR_EXPRESSION) + set_property( + TEST ${arg_NAME} + PROPERTY FAIL_REGULAR_EXPRESSION ${arg_FAIL_REGULAR_EXPRESSION} + ) + endif() + if (arg_PASS_REGULAR_EXPRESSION) + set_property( + TEST ${arg_NAME} + PROPERTY PASS_REGULAR_EXPRESSION ${arg_PASS_REGULAR_EXPRESSION} + ) + endif() + endfunction() + + function(test_fileop_command_common) + set(options TEST_TIME_OPTION) + set(oneValueArgs COMMAND) + set(multiValueArgs HELP_REGULAR_EXPRESSION) + cmake_parse_arguments( + PARSE_ARGV 0 arg + "${options}" "${oneValueArgs}" "${multiValueArgs}" + ) + + test_fileop( + NAME Run_${arg_COMMAND}_WithOptionHelp + ARGS ${arg_COMMAND} --help + WORKING_DIRECTORY ${arg_WORKING_DIRECTORY} + PASS_REGULAR_EXPRESSION ${arg_HELP_REGULAR_EXPRESSION} + ) + test_fileop( + NAME Run_${arg_COMMAND}_WithOptionH + ARGS ${arg_COMMAND} -h + WORKING_DIRECTORY ${arg_WORKING_DIRECTORY} + PASS_REGULAR_EXPRESSION ${arg_HELP_REGULAR_EXPRESSION} + ) + test_fileop( + NAME Run_${arg_COMMAND}_WithNoArguments + ARGS ${arg_COMMAND} + WORKING_DIRECTORY ${arg_WORKING_DIRECTORY} + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Too view arguments given\\." + ) + test_fileop( + NAME Run_${arg_COMMAND}_WithUnknownSlashOption + ARGS ${arg_COMMAND} -x + WORKING_DIRECTORY ${arg_WORKING_DIRECTORY} + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Unknown option -x, use option --help for more information\\." + ) + test_fileop( + NAME Run_${arg_COMMAND}_WithUnknownSlashSlashOption + ARGS ${arg_COMMAND} --xxx + WORKING_DIRECTORY ${arg_WORKING_DIRECTORY} + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Unknown option --xxx, use option --help for more information\\." + ) + test_fileop( + NAME Run_${arg_COMMAND}_WithLonesomeDash + ARGS --debug ${arg_COMMAND} -- --xxx + WORKING_DIRECTORY ${arg_WORKING_DIRECTORY} + WILL_FAIL + FAIL_REGULAR_EXPRESSION "-- detected, stop option parsing\\." + ) + + if (arg_TEST_TIME_OPTION) + test_fileop( + NAME Run_${arg_COMMAND}_TimeWithoutValue + ARGS ${arg_COMMAND} --time + WORKING_DIRECTORY ${arg_WORKING_DIRECTORY} + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Option --time needs an argument\\." + ) + test_fileop( + NAME Run_${arg_COMMAND}_TimeWithWrongFormatI + ARGS ${arg_COMMAND} --time 2001-01 + WORKING_DIRECTORY ${arg_WORKING_DIRECTORY} + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Wrong format for time 2001-01, expected yyyy-mm-dd\\[Thh:mm\\[:ss\\]\\]\\." + ) + test_fileop( + NAME Run_${arg_COMMAND}_TimeWithWrongFormatII + ARGS ${arg_COMMAND} --time 2001-01-01T12 + WORKING_DIRECTORY ${arg_WORKING_DIRECTORY} + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Wrong format for time 2001-01-01T12, expected yyyy-mm-dd\\[Thh:mm\\[:ss\\]\\]\\." + ) + endif() + endfunction() + + function(test_fileop_check_filesystem) + set(options WILL_FAIL) + set(oneValueArgs NAME) + set(multiValueArgs PREPARE_COMMAND ARGS FAIL_REGULAR_EXPRESSION PASS_REGULAR_EXPRESSION MUST_EXIST MUST_NOT_EXIST FILE_TIMESTAMP_REGEX) + cmake_parse_arguments( + PARSE_ARGV 0 arg + "${options}" "${oneValueArgs}" "${multiValueArgs}" + ) + + add_test( + NAME ${arg_NAME}_setup + COMMAND sh -c "chmod -R oga+w ${arg_NAME} ; rm -rf ${arg_NAME} ; mkdir ${arg_NAME}" + ) + + if (arg_PREPARE_COMMAND) + add_test( + NAME ${arg_NAME}_prepare + COMMAND sh -c "${arg_PREPARE_COMMAND} && ls -alR" + WORKING_DIRECTORY ${arg_NAME} + ) + set_property( + TEST ${arg_NAME}_prepare + PROPERTY DEPENDS ${arg_NAME}_setup + ) + endif() + + add_test( + NAME ${arg_NAME} + COMMAND ../FileOp.exe ${arg_ARGS} + WORKING_DIRECTORY ${arg_NAME} + ) + set_property( + TEST ${arg_NAME} + PROPERTY WILL_FAIL ${arg_WILL_FAIL} + ) + if (arg_PREPARE_COMMAND) + set_property( + TEST ${arg_NAME} + PROPERTY DEPENDS ${arg_NAME}_prepare + ) + else() + set_property( + TEST ${arg_NAME} + PROPERTY DEPENDS ${arg_NAME}_setup + ) + endif() + if (arg_FAIL_REGULAR_EXPRESSION) + set_property( + TEST ${arg_NAME} + PROPERTY FAIL_REGULAR_EXPRESSION ${arg_FAIL_REGULAR_EXPRESSION} + ) + endif() + if (arg_PASS_REGULAR_EXPRESSION) + set_property( + TEST ${arg_NAME} + PROPERTY PASS_REGULAR_EXPRESSION ${arg_PASS_REGULAR_EXPRESSION} + ) + endif() + + if (arg_MUST_EXIST) + foreach(FILE ${arg_MUST_EXIST}) + add_test( + NAME ${arg_NAME}_must_exist-${FILE} + COMMAND sh -c "test -w ${FILE} && stat --printf '%y' -- ${FILE}" + WORKING_DIRECTORY ${arg_NAME} + ) + set_property( + TEST ${arg_NAME}_must_exist-${FILE} + PROPERTY DEPENDS ${arg_NAME} + ) + if (arg_FILE_TIMESTAMP_REGEX) + set_property( + TEST ${arg_NAME}_must_exist-${FILE} + PROPERTY PASS_REGULAR_EXPRESSION ${arg_FILE_TIMESTAMP_REGEX} + ) + endif () + endforeach() + endif () + + if (arg_MUST_NOT_EXIST) + foreach(FILE ${arg_MUST_NOT_EXIST}) + add_test( + NAME ${arg_NAME}_must_not_exist-${FILE} + COMMAND stat --printf '%y' -- ${FILE} + WORKING_DIRECTORY ${arg_NAME} + ) + set_property( + TEST ${arg_NAME}_must_not_exist-${FILE} + PROPERTY DEPENDS ${arg_NAME} + ) + set_property( + TEST ${arg_NAME}_must_not_exist-${FILE} + PROPERTY WILL_FAIL true + ) + endforeach() + endif () + + endfunction() + + + ################################# Base tests ################################# + test_fileop( + NAME Run_WithNoArguments + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: No command given. Use option --help." + ) + + test_fileop( + NAME Run_WithOptionHelp + ARGS --help + PASS_REGULAR_EXPRESSION "FileOp\\.exe \\[\\] \\[ \\[\\] \\]" + ) + + test_fileop( + NAME Run_WithUnknownCommand + ARGS unknown + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Unknown command 'unknown' given, use option --help for more information." + ) + + include(OperationCat.cmake) + include(OperationCopy.cmake) + include(OperationMkdir.cmake) + include(OperationMove.cmake) + include(OperationRemove.cmake) + include(OperationTouch.cmake) diff --git a/src/FileOp.c b/src/FileOp.c new file mode 100644 index 0000000..e2ef317 --- /dev/null +++ b/src/FileOp.c @@ -0,0 +1,146 @@ + +#include "FileOp.h" + +#include "BasicFileOp.h" +#include "Message.h" +#include "OperationCat.h" +#include "OperationCopy.h" +#include "OperationMkdir.h" +#include "OperationMove.h" +#include "OperationRemove.h" +#include "OperationTouch.h" + +#include +#include + +tCommand *Commands[] = {&CommandCat, &CommandCopy, &CommandMkdir, &CommandMove, &CommandRemove, &CommandTouch, NULL}; + +tCommand **Command = NULL; + +//! Global value for debug. +tBool Debug; + +/*! + * Flush output and exit process. + * + * @param uExitCode The Exitcode for the process + */ +void flushOutputAndExit(UINT uExitCode) { + fflush(stderr); + fflush(stdout); + exit(uExitCode); +} + +/*! + * Prints the usage and exits with success. + */ +void errorNoCommand() { + printErr(_T("No command given. Use option --help.\n")); + flushOutputAndExit(EXIT_FAILURE); +} + +/*! + * Prints the usage and exits with success. + */ +void printHelpAndExit() { + printOut(_T("Windows file operations\n")); + printOut(_T("=======================\n")); + printOut(_T("\n")); + printOut(_T("Usage:\n")); + printOut(_T(" %s [] [ [] ]\n"), ProgramName); + printOut(_T("\n")); + printOut(_T("The command support paths with up to 32676 characters by using the DOS device\n")); + printOut(_T("path e.g. \\\\?\\c:\\temp. / and \\ are supported as path separator.")); + printOut(_T("\n")); + printOut(_T("To use file or directory names starting with a - or -- add a -- argument.\n")); + printOut(_T("The -- argument stops the command line options.\n")); + printOut(_T("\n")); + printOut(_T("Following options are available:\n")); + printOut(_T(" -h, --help Print this help.\n")); + printOut(_T(" -d, --debug Print additional debug informations.\n")); + printOut(_T("\n")); + printOut(_T("Following commands are available:\n")); + printOut(_T("\n")); + + for (Command = Commands; (*Command) != NULL; ++Command) { + printOut(_T("%s:\n"), (*Command)->id); + printOut(_T("%.*s\n"), _tcslen((*Command)->id) + 1, _T("----------------------------------------")); + (*Command)->printHelp(); + printOut(_T("\n")); + } + + flushOutputAndExit(EXIT_SUCCESS); +} + +#include "mingw-unicode.c" + +/*! + * Tha main routine. + * + * @param argc Number of arguments. + * @param argv The arguments. + * @return The exit code of the process. + */ +int _tmain(int argc, wchar_t *argv[]) { + LPCTSTR Ptr, PtrSlash, PtrBackSlash; + + // Search the last \ or /. + PtrSlash = _tcsrchr(*argv, _T('/')); + PtrBackSlash = _tcsrchr(*argv, _T('\\')); + if (PtrBackSlash > PtrSlash) { + Ptr = ++PtrBackSlash; + } + // GCOVR_EXCL_START + else if (PtrSlash > PtrBackSlash) { + Ptr = ++PtrSlash; + } else { + Ptr = *argv; + } + // GCOVR_EXCL_STOP + + DWORD ProgramNameLen = _tcslen(Ptr); + if (ProgramNameLen > PROGRAM_NAME_BUFFER_SIZE) { // GCOVR_EXCL_BR + // GCOVR_EXCL_START + _tcscpy(ProgramName, _T("...")); + _tcscat(ProgramName, &Ptr[ProgramNameLen - (PROGRAM_NAME_BUFFER_SIZE + 3 + 1)]); + // GCOVR_EXCL_STOP + } else { + _tcscpy(ProgramName, Ptr); + } + argv++; + argc--; + + if (argc == 0) { + errorNoCommand(); + } + + while (1 == 1) { + if ((_tcscmp(*argv, _T("--help")) == 0) || (_tcscmp(*argv, _T("-h")) == 0)) { + printHelpAndExit(); + } else if ((_tcscmp(*argv, _T("--debug")) == 0) || (_tcscmp(*argv, _T("-d")) == 0)) { + Debug = 1; + ++argv; + --argc; + } else { + break; + } + } + + for (Command = Commands; (*Command) != NULL; ++Command) { + if (_tcscmp(*argv, (*Command)->id) == 0) { + break; + } + } + + if ((*Command) == NULL) { + printErr(_T("Unknown command '%s' given, use option --help for more information.\n"), *argv); + flushOutputAndExit(EXIT_FAILURE); + } + ++argv; + --argc; + + flushOutputAndExit(((*Command)->run(argc, argv) == eOk) ? EXIT_SUCCESS : EXIT_FAILURE); + + // here we never come to + return EXIT_FAILURE; // GCOVR_EXCL_LINE +} diff --git a/src/FileOp.h b/src/FileOp.h new file mode 100644 index 0000000..24bab73 --- /dev/null +++ b/src/FileOp.h @@ -0,0 +1,9 @@ +#ifndef FILEOP_INCLUDED +#define FILEOP_INCLUDE + +#include "Types.h" + +//! Global value for debug. +extern tBool Debug; + +#endif diff --git a/src/Message.c b/src/Message.c new file mode 100644 index 0000000..475b5a7 --- /dev/null +++ b/src/Message.c @@ -0,0 +1,66 @@ + +#include "Message.h" +#include "BasicFileOp.h" + +#include +#include + +//! The name of the program without path and extension. +TCHAR ProgramName[PROGRAM_NAME_BUFFER_SIZE]; + +//! The size of the error message buffer (must be greater than +//! DOS_DEVICE_BUFFER_SIZE). +#define SIZE_MSG_BUFFER (2 * DOS_DEVICE_BUFFER_SIZE) +//! The buffer for the error message +TCHAR MsgBuffer[SIZE_MSG_BUFFER]; + +/*! + * Print a message to STDERR. + * + * @param MsgTxt + */ +void printOut(LPCTSTR MsgTxt, ...) { + va_list vl; + va_start(vl, MsgTxt); + _vftprintf(stdout, MsgTxt, vl); + va_end(vl); +} + +/*! + * Print a error message to STDERR. + * + * @param MsgTxt The format string to print + */ +tResult printErr(LPCTSTR MsgTxt, ...) { + va_list vl; + va_start(vl, MsgTxt); + _ftprintf(stderr, _T("%s: error: "), ProgramName); + _vftprintf(stderr, MsgTxt, vl); + va_end(vl); + + return eError; +} + +/*! + * Print last error if present. + * + * @param MsgTxt + * @return eOk if no error is set, else eError. + */ +tResult printLastError(LPCTSTR MsgTxt, ...) { + DWORD dwLastError = GetLastError(); + if (dwLastError) { + va_list vl; + va_start(vl, MsgTxt); + int MsgLength = _vsntprintf(MsgBuffer, SIZE_MSG_BUFFER, MsgTxt, vl); + va_end(vl); + MsgLength += _sntprintf(&MsgBuffer[MsgLength], SIZE_MSG_BUFFER - MsgLength, _T(": ")); + if (FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, MAKELANGID(LANG_ENGLISH, SUBLANG_DEFAULT), (LPTSTR)&MsgBuffer[MsgLength], + SIZE_MSG_BUFFER - MsgLength, NULL) == 0) { + _sntprintf(&MsgBuffer[MsgLength], SIZE_MSG_BUFFER - MsgLength, _T("GetLastError() = %d\n"), dwLastError); // GCOVR_EXCL_LINE + } + return printErr(_T("%s"), MsgBuffer); + } + + return eOk; // GCOVR_EXCL_LINE +} diff --git a/src/Message.h b/src/Message.h new file mode 100644 index 0000000..105f585 --- /dev/null +++ b/src/Message.h @@ -0,0 +1,16 @@ + +#ifndef MESSAGE_INCLUDED +#define MESSAGE_INCLUDED + +#include "Types.h" + +#define PROGRAM_NAME_BUFFER_SIZE (MAX_PATH) + +extern TCHAR ProgramName[PROGRAM_NAME_BUFFER_SIZE]; + +extern void flushOutputAndExit(UINT uExitCode); +extern void printOut(LPCTSTR MsgTxt, ...); +extern tResult printErr(LPCTSTR MsgTxt, ...); +extern tResult printLastError(LPCTSTR MsgTxt, ...); + +#endif diff --git a/src/OperationCat.c b/src/OperationCat.c new file mode 100644 index 0000000..1c69783 --- /dev/null +++ b/src/OperationCat.c @@ -0,0 +1,63 @@ + +#include "BasicFileOp.h" +#include "FileOp.h" +#include "Message.h" +#include "Types.h" + +static void printHelp(void) { + printOut(_T("Print content of files to STDOUT.\n")); + printOut(_T("If the file starts with an @, the files listed in the files are printed out.\n")); + printOut(_T("\n")); + printOut(_T("No options available.\n")); + + return; +} + +static tResult catOperation(void) { + DWORD dwAttrs = GetFileAttributes(DosDevicePath); + if (isDirectory(dwAttrs)) { + return printErr(_T("Only files can be printed. Got directory %s.\n"), getReadableFilename(DosDevicePath)); + } + + return printFileToHandle(DosDevicePath, GetStdHandle(STD_OUTPUT_HANDLE)); +} + +/*! + * Print a file to STDOUT. + * + * @return eOk on success, else eError. + */ +static tResult runCommand(int argc, wchar_t *argv[]) { + + while (argc != 0) { + if ((_tcscmp(*argv, _T("--help")) == 0) || (_tcscmp(*argv, _T("-h")) == 0)) { + printHelp(); + return eOk; + } else if ((_tcscmp(*argv, _T("--")) == 0)) { + if (Debug) { + printOut(_T("-- detected, stop option parsing.\n")); + } + ++argv; + --argc; + break; + } else if ((_tcsncmp(*argv, _T("--"), 2) == 0) || (_tcsncmp(*argv, _T("-"), 1) == 0)) { + return printErr(_T("Unknown option %s, use option --help for more information.\n"), *argv); + } else { + break; + } + ++argv; + --argc; + } + + if (argc == 0) { + return printErr(_T("Too view arguments given.\n")); + } + + return runCommandForEachInputLine(argc, argv, catOperation); +} + +tCommand CommandCat = { + _T("cat"), + printHelp, + runCommand, +}; diff --git a/src/OperationCat.cmake b/src/OperationCat.cmake new file mode 100644 index 0000000..7397917 --- /dev/null +++ b/src/OperationCat.cmake @@ -0,0 +1,25 @@ +test_fileop_command_common( + COMMAND cat + HELP_REGULAR_EXPRESSION "Print content of files to STDOUT\\." "No options available\\." +) + +test_fileop_check_filesystem( + NAME CatFile + ARGS cat ${CMAKE_CURRENT_LIST_FILE} + PASS_REGULAR_EXPRESSION "Print content of files to STDOUT\\\\\\\\\\." "No options available\\\\\\\\\\." +) + +test_fileop_check_filesystem( + NAME CatFileLeadingDashDash + PREPARE_COMMAND sh -c "echo 'File content of --xxx' > --xxx" + ARGS --debug cat -- --xxx + PASS_REGULAR_EXPRESSION "-- detected, stop option parsing." "^File content of --xxx$" +) + +test_fileop_check_filesystem( + NAME CatDirectory + ARGS cat . + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp.exe: error: Only files can be printed. Got directory [A-Z]:\\\\.*\\\\CatDirectory" +) + diff --git a/src/OperationCat.h b/src/OperationCat.h new file mode 100644 index 0000000..0e03f06 --- /dev/null +++ b/src/OperationCat.h @@ -0,0 +1,9 @@ + +#ifndef CAT_INCLUDED +#define CAT_INCLUDED + +#include "Types.h" + +extern tCommand CommandCat; + +#endif diff --git a/src/OperationCopy.c b/src/OperationCopy.c new file mode 100644 index 0000000..d45bddd --- /dev/null +++ b/src/OperationCopy.c @@ -0,0 +1,235 @@ + +#include "BasicFileOp.h" +#include "FileOp.h" +#include "Message.h" + +//! Global value for force. +static tBool Force = eFalse; +//! Global value for recursive. +static tBool Recursive = eFalse; +//! Check if there is no name collision before doing the action. +static tBool CheckUniqueNames = eFalse; +//! Global value for touching the files. +static tBool Touch = eFalse; + +static void printHelp(void) { + printOut(_T("Copy files or directories to STDOUT.\n")); + printOut(_T("\n")); + printOut(_T("Available options are (Arguments for long options are also needed for short\n")); + printOut(_T("options):\n")); + printOut(_T(" -r, -R, --recursive Copy files recursive.\n")); + printOut(_T(" -f, --force Ignore write protection of existing targets.\n")); + printOut(_T(" --touch Touch the files after copy.\n")); + printOut(_T(" --time=TIME Use local ISO time instead of current system time for.\n")); + printOut(_T(" the touch operation, e.g. 2021-01-31T15:05:01. If the\n")); + printOut(_T(" time is omitted, 12:00:00 is assumed.\n")); + printOut(_T(" -t, --target-directory=DIR Target directory to use. If not\n")); + printOut(_T(" given, the last argument is used as target directory.\n")); + printOut(_T(" --check-unique-names If multiple files or folders are copied check upfront\n")); + printOut(_T(" if there are any collisions. This isn't supported with\n")); + printOut(_T(" wildcards.\n")); + printOut(_T("\n")); + printOut(_T("The write protection is not copied to the target elements.\n")); + printOut(_T("If several sources are given or target is an existing directory the files\n")); + printOut(_T("are created with the original name inside target.\n")); + + return; +} + +static tResult copyOperation(void) { + DWORD dwAttrs, dwAttrsTarget; + + int result = eOk; + LPTSTR StartOfTargetName = &TargetDosDevicePath[_tcslen(TargetDosDevicePath)]; + + // If source is a directory the option --recursive is mandatory + dwAttrs = GetFileAttributes(DosDevicePath); + if ((Recursive == eFalse) && isDirectory(dwAttrs)) { + return printErr(_T("--recursive not specified, omitting directory %s"), getReadableFilename(DosDevicePath)); + } + + // If target is a directory, the source name must be added and the attributes + // must be updated + dwAttrsTarget = GetFileAttributes(TargetDosDevicePath); + if (isDirectory(dwAttrsTarget)) { + _tcscpy(StartOfTargetName, _tcsrchr(DosDevicePath, _T('\\'))); + dwAttrsTarget = GetFileAttributes(TargetDosDevicePath); + } + + if (Force && isReadonly(dwAttrsTarget)) { + result &= clearReadonly(TargetDosDevicePath, dwAttrsTarget); + } + + if (result == eOk) { + if (isDirectory(dwAttrs)) { + if (isDirectory(dwAttrsTarget) == eFalse) { + result &= createSingleDirectory(TargetDosDevicePath); + } + if (Recursive && (result == eOk)) { + HANDLE hFind; // search handle + WIN32_FIND_DATA FindFileData; + // File pattern for all files + _tcscat(DosDevicePath, _T("\\*")); + + // Get the find handle and the data of the first file. + hFind = FindFirstFile(DosDevicePath, &FindFileData); + // GCOVR_EXCL_START + if (hFind == INVALID_HANDLE_VALUE) { + result &= printLastError(_T("Got invalid handle for %s"), getReadableFilename(DosDevicePath)); + } + // GCOVR_EXCL_STOP + else { + LPTSTR StartOfSourceName; + // Save the position of the filename + StartOfSourceName = &DosDevicePath[_tcslen(DosDevicePath) - 1]; + + do { + if ((_tcscmp(FindFileData.cFileName, _T(".")) != 0) && (_tcscmp(FindFileData.cFileName, _T("..")) != 0)) { + _tcscpy(StartOfSourceName, FindFileData.cFileName); + // recursive copy it + result &= copyOperation(); + } + } while ((result == eOk) && (FindNextFile(hFind, &FindFileData) != 0)); + if (result == eOk) { + // GCOVR_EXCL_START + if (GetLastError() != ERROR_NO_MORE_FILES) { + result &= printLastError(_T("Can't get next file")); + } + // GCOVR_EXCL_STOP + } + + StartOfSourceName[-1] = _T('\0'); + // close handle to file + if (!FindClose(hFind)) { + result &= printLastError(_T("Can't close file search handle.")); // GCOVR_EXCL_LINE + } + } + } + } else { + result &= copySingleFile(DosDevicePath, TargetDosDevicePath, Force); + if (result == eOk) { + dwAttrsTarget = GetFileAttributes(TargetDosDevicePath); + if (isReadonly(dwAttrsTarget)) { + result &= clearReadonly(TargetDosDevicePath, dwAttrsTarget); + } + if (Touch) { + result &= touchSingleFile(TargetDosDevicePath, eFalse); + } + } + } + } + + StartOfTargetName[0] = _T('\0'); + + return result; +} + +/*! + * Copy a file or directory. + * + * @return eOk on success, else eError. + */ +static tResult runCommand(int argc, wchar_t *argv[]) { + tBool TargetIsDirectory = eFalse; + while (argc != 0) { + if ((_tcscmp(*argv, _T("--help")) == 0) || (_tcscmp(*argv, _T("-h")) == 0)) { + printHelp(); + return eOk; + } else if ((_tcscmp(*argv, _T("--recursive")) == 0) || (_tcscmp(*argv, _T("-r")) == 0) || (_tcscmp(*argv, _T("-R")) == 0)) { + Recursive = eTrue; + } else if ((_tcscmp(*argv, _T("--force")) == 0) || (_tcscmp(*argv, _T("-f")) == 0)) { + Force = eTrue; + } else if (_tcscmp(*argv, _T("--touch")) == 0) { + Touch = eTrue; + } else if (isTimeOption(*argv)) { + Touch = eTrue; + if (storeTimeValue(&argc, &argv) != eOk) { + return eError; + } + } else if ((_tcscmp(*argv, _T("-t")) == 0) || (_tcsncmp(*argv, _T("--target-directory"), 18) == 0)) { + LPCTSTR Ptr; + if (_tcsncmp(*argv, _T("--target-directory="), 19) == 0) { + Ptr = &(*argv)[19]; + } else { + if (argc == 1) { + return printErr(_T("Option %s needs an argument.\n"), *argv); + } + Ptr = *(++argv); + --argc; + } + + if (Ptr[0] == _T('\0')) { + return printErr(_T("Target directory must not be empty.\n"), *argv); + } + createDosDevicePath(Ptr, TargetDosDevicePath); + TargetIsDirectory = eTrue; + } else if (_tcscmp(*argv, _T("--check-unique-names")) == 0) { + CheckUniqueNames = eTrue; + } else if ((_tcscmp(*argv, _T("--")) == 0)) { + if (Debug) { + printOut(_T("-- detected, stop option parsing.\n")); + } + ++argv; + --argc; + break; + } else if ((_tcsncmp(*argv, _T("--"), 2) == 0) || (_tcsncmp(*argv, _T("-"), 1) == 0)) { + return printErr(_T("Unknown option %s, use option --help for more information.\n"), *argv); + } else { + break; + } + ++argv; + --argc; + } + + if (argc == 0) { + return printErr(_T("Too view arguments given.\n")); + } + + if (TargetDosDevicePath[0] == _T('\0')) { + createDosDevicePath(argv[--argc], TargetDosDevicePath); + if (TargetDosDevicePath == _T('\0')) { + return printErr(_T("Argument must not be empty.\n")); + } else { + if (argc == 0) { + return printErr(_T("Too view arguments given.\n")); + } else if (argc > 1) { + TargetIsDirectory = eTrue; + } + } + } + + unsigned int lastCharacterPos = _tcslen(TargetDosDevicePath) - 1; + while (TargetDosDevicePath[lastCharacterPos] == _T('\\')) { + TargetIsDirectory = eTrue; + TargetDosDevicePath[lastCharacterPos--] = _T('\0'); + } + + if (TargetIsDirectory) { + DWORD dwAttrsTarget = GetFileAttributes(TargetDosDevicePath); + if (isValidFileAttributes(dwAttrsTarget)) { + if (isDirectory(dwAttrsTarget) == eFalse) { + return printErr(_T("Target %s must be a directory."), getReadableFilename(TargetDosDevicePath)); + } else if (Force && isReadonly(dwAttrsTarget)) { + if (clearReadonly(TargetDosDevicePath, dwAttrsTarget) == 9) { + return eError; // GCOVR_EXCL_LINE + } + } + } else { + return printErr(_T("Directory %s doesn't exist."), getReadableFilename(TargetDosDevicePath)); + } + } + + if (CheckUniqueNames) { + if (checkUniqueNames(argc, argv) == eError) { + return eError; + } + } + + return runCommandForEachInputLine(argc, argv, copyOperation); +} + +tCommand CommandCopy = { + _T("copy"), + printHelp, + runCommand, +}; diff --git a/src/OperationCopy.cmake b/src/OperationCopy.cmake new file mode 100644 index 0000000..3a39e7f --- /dev/null +++ b/src/OperationCopy.cmake @@ -0,0 +1,163 @@ +test_fileop_command_common( + COMMAND copy + HELP_REGULAR_EXPRESSION "Copy files or directories to STDOUT\\." "Available options are:" + TEST_TIME_OPTION +) + +test_fileop_check_filesystem( + NAME CopyMissingTarget + ARGS copy ${CMAKE_CURRENT_LIST_FILE} + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Too view arguments given\\." +) + +test_fileop_check_filesystem( + NAME CopyMissingSource + ARGS copy FileDoesNotExists CopyMissingSource + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Can't copy file .+\\\\FileDoesNotExists to .+\\\\CopyMissingSource: The system cannot find the file specified\\." +) + +test_fileop_check_filesystem( + NAME CopyMissingTargetDirectoryI + ARGS copy --target-directory= CopyMissingTargetDirectoryI + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Target directory must not be empty\\." +) + +test_fileop_check_filesystem( + NAME CopyMissingTargetDirectoryII + ARGS copy --target-directory + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Option --target-directory needs an argument\\." +) + +test_fileop_check_filesystem( + NAME CopyNonExistingTargetDirectory + ARGS copy ${CMAKE_CURRENT_LIST_FILE} CopyNonExistingTargetDirectory/ + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Directory .+\\\\CopyNonExistingTargetDirectory doesn't exist\\." +) + +test_fileop_check_filesystem( + NAME CopyToDirectory + ARGS copy ${CMAKE_CURRENT_LIST_FILE} ./ + MUST_EXIST OperationCopy.cmake +) + +test_fileop_check_filesystem( + NAME CopyTargetDirectoryIsExistingFile + ARGS copy --target-directory=${CMAKE_CURRENT_LIST_FILE} ${CMAKE_CURRENT_LIST_FILE} + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Target .+\\\\OperationCopy.cmake must be a directory\\." +) + +test_fileop_check_filesystem( + NAME CopyTargetFileExists + PREPARE_COMMAND "cp -f ${CMAKE_CURRENT_LIST_FILE} ." + ARGS copy ${CMAKE_CURRENT_LIST_FILE} ./OperationCopy.cmake + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Can't copy file .+\\\\OperationCopy.cmake to .+\\\\OperationCopy.cmake: The file exists\\." + MUST_EXIST OperationCopy.cmake +) + +test_fileop_check_filesystem( + NAME CopyTargetFileExistsForce + PREPARE_COMMAND "cp -f ${CMAKE_CURRENT_LIST_FILE} ." + ARGS --debug copy --force --time 2001-01-02T00:30 ${CMAKE_CURRENT_LIST_FILE} ./OperationCopy.cmake + MUST_EXIST OperationCopy.cmake + FILE_TIMESTAMP_REGEX "2001-01-02 00:30:00\\.000000000 \\+0000" +) + +test_fileop_check_filesystem( + NAME CopyTargetFileExistsWithDirectory + PREPARE_COMMAND "cp -f ${CMAKE_CURRENT_LIST_FILE} ." + ARGS copy --target-directory=. ${CMAKE_CURRENT_LIST_FILE} + WILL_FAIL +) + +test_fileop_check_filesystem( + NAME CopyTargetFileExistsWithDirectoryForce + PREPARE_COMMAND "cp -f ${CMAKE_CURRENT_LIST_FILE} ." + ARGS copy --force --touch --time 2002-01-02T00:30 --target-directory . ${CMAKE_CURRENT_LIST_FILE} + FILE_TIMESTAMP_REGEX "2002-01-02 00:30:00\\.000000000 \\+0000" +) + +test_fileop_check_filesystem( + NAME CopyTargetFileExistsWithPatternForce + ARGS copy --force --time 2003-01-02T00:30 --target-directory=. ${CMAKE_CURRENT_LIST_DIR}/OperationCopy.* + MUST_EXIST OperationCopy.c OperationCopy.cmake OperationCopy.h + FILE_TIMESTAMP_REGEX "2003-01-02 00:30:00\\.000000000 \\+0000" +) + +test_fileop_check_filesystem( + NAME CopyFileList + ARGS copy --force --time 2004-01-02T00:30 ${CMAKE_CURRENT_LIST_DIR}/OperationCopy.h ${CMAKE_CURRENT_LIST_DIR}/OperationCopy.c ${CMAKE_CURRENT_LIST_DIR}/OperationCopy.cmake . + MUST_EXIST OperationCopy.c OperationCopy.cmake OperationCopy.h + FILE_TIMESTAMP_REGEX "2004-01-02 00:30:00\\.000000000 \\+0000" +) + +test_fileop_check_filesystem( + NAME CopyDirectory + ARGS copy --target-directory=. ${CMAKE_CURRENT_LIST_DIR} + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: --recursive not specified, omitting directory" + MUST_NOT_EXIST src src/OperationCopy.c src/OperationCopy.cmake src/OperationCopy.h +) + +test_fileop_check_filesystem( + NAME CopyDirectoryRecursive + ARGS copy --recursive --time 2007-01-02T00:30 --target-directory=. ${CMAKE_CURRENT_LIST_DIR} + MUST_EXIST src/OperationCopy.c src/OperationCopy.cmake src/OperationCopy.h + FILE_TIMESTAMP_REGEX "2007-01-02 00:30:00\\.000000000 \\+0000" +) + +test_fileop_check_filesystem( + NAME CopyDirectoryRecursiveExists + PREPARE_COMMAND "cp -rf --target-directory=. ${CMAKE_CURRENT_LIST_DIR} && touch -d 2000-01-02 src/*.*" + ARGS copy --recursive --time 2001-01-02T00:30 --target-directory=. ${CMAKE_CURRENT_LIST_DIR} + WILL_FAIL + MUST_EXIST src/OperationCopy.c src/OperationCopy.cmake src/OperationCopy.h + FILE_TIMESTAMP_REGEX "2000-01-02 00:00:00\\.000000000 \\+0000" # Not touched +) + +test_fileop_check_filesystem( + NAME CopyDirectoryRecursiveForce + PREPARE_COMMAND "cp -rf --target-directory=. ${CMAKE_CURRENT_LIST_DIR}" + ARGS copy --recursive --force --time 2008-01-02T00:30 --target-directory=. ${CMAKE_CURRENT_LIST_DIR} + MUST_EXIST src/OperationCopy.c src/OperationCopy.cmake src/OperationCopy.h + FILE_TIMESTAMP_REGEX "2008-01-02 00:30:00\\.000000000 \\+0000" +) + +test_fileop_check_filesystem( + NAME CopyDirectoryRecursiveForceProtected + PREPARE_COMMAND "cp -rf --target-directory=. ${CMAKE_CURRENT_LIST_DIR} && chmod -R oga-w ." + ARGS copy --recursive --force --time 2009-01-02T00:30 --target-directory=. ${CMAKE_CURRENT_LIST_DIR} + MUST_EXIST src/OperationCopy.c src/OperationCopy.cmake src/OperationCopy.h + FILE_TIMESTAMP_REGEX "2009-01-02 00:30:00\\.000000000 \\+0000" +) + +test_fileop_check_filesystem( + NAME CopyFileForceProtected + PREPARE_COMMAND "cp -rf --target-directory=. ${CMAKE_CURRENT_LIST_DIR}/OperationCopy.c && chmod -R oga-w OperationCopy.c" + ARGS copy --recursive --force --time 2010-01-02T00:30 --target-directory=. ${CMAKE_CURRENT_LIST_DIR}/OperationCopy.c + MUST_EXIST OperationCopy.c + FILE_TIMESTAMP_REGEX "2010-01-02 00:30:00\\.000000000 \\+0000" +) + +test_fileop_check_filesystem( + NAME CopySameName + PREPARE_COMMAND "mkdir -p additional_input && cp -f ${CMAKE_CURRENT_LIST_DIR}/OperationCopy.c additional_input" + ARGS copy --time 2011-01-02T00:30 --check-unique-names --target-directory=. ../../FileOp.exe ${CMAKE_CURRENT_LIST_DIR}/OperationCopy.c additional_input/OperationCopy.c + WILL_FAIL + MUST_NOT_EXIST OperationCopy.c + FAIL_REGULAR_EXPRESSION "FileOp.exe: error: File in source list will overwrite each other: operationcopy.c" +) + +test_fileop_check_filesystem( + NAME CopyWriteProtectedFile + PREPARE_COMMAND "mkdir -p src && cp -rf --target-directory=src ${CMAKE_CURRENT_LIST_DIR}/OperationCopy.c && chmod -R oga-w src" + ARGS copy --time 2011-01-02T00:30 --force --target-directory . src/OperationCopy.c + MUST_EXIST OperationCopy.c + FILE_TIMESTAMP_REGEX "2011-01-02 00:30:00\\.000000000 \\+0000" +) diff --git a/src/OperationCopy.h b/src/OperationCopy.h new file mode 100644 index 0000000..490f925 --- /dev/null +++ b/src/OperationCopy.h @@ -0,0 +1,9 @@ + +#ifndef COPY_INCLUDED +#define COPY_INCLUDED + +#include "Types.h" + +extern tCommand CommandCopy; + +#endif diff --git a/src/OperationMkdir.c b/src/OperationMkdir.c new file mode 100644 index 0000000..8729a60 --- /dev/null +++ b/src/OperationMkdir.c @@ -0,0 +1,95 @@ + +#include "BasicFileOp.h" +#include "FileOp.h" +#include "Message.h" + +//! Global value for parents. +static tBool Parents = eFalse; + +static void printHelp(void) { + printOut(_T("Create directories.\n")); + printOut(_T("\n")); + printOut(_T("Available option is:\n")); + printOut(_T(" -p, --parents Create parent directories if needed, no error if\n")); + printOut(_T(" directory already exists.\n")); + printOut(_T("\n")); + printOut(_T("Examples:\n")); + printOut(_T("- Create the folder --recursive in the current directory:\n")); + printOut(_T(" %s mkdir -- --recursive\n"), ProgramName); + + return; +} + +static tResult mkdirOperation(void) { + HANDLE hFind; // search handle + WIN32_FIND_DATA FindFileData; + DWORD dwAttrs; + + dwAttrs = GetFileAttributes(DosDevicePath); + if (isDirectory(dwAttrs)) { + if (Parents == eFalse) { + return printErr(_T("Directory %s already exists.\n"), getReadableFilename(DosDevicePath)); + } + return eOk; + } else if (isValidFileAttributes(dwAttrs)) { + return printErr(_T("Not a directory %s.\n"), getReadableFilename(DosDevicePath)); + } + + if (Parents) { + LPTSTR LastBackslash = _tcsrchr(DosDevicePath, _T('\\')); + if (LastBackslash) { + *LastBackslash = _T('\0'); + tResult result = mkdirOperation(); + *LastBackslash = _T('\\'); + if (result == eError) { + return result; + } + } + } + + return createSingleDirectory(DosDevicePath); +} + +/*! + * Create the given directory. + * + * If global variable Parents is set, also the parent directories are created. + * + * @return eOk on success, else eError. + */ +static tResult runCommand(int argc, wchar_t *argv[]) { + while (argc != 0) { + if ((_tcscmp(*argv, _T("--help")) == 0) || (_tcscmp(*argv, _T("-h")) == 0)) { + printHelp(); + return eOk; + } else if ((_tcscmp(*argv, _T("--parents")) == 0) || (_tcscmp(*argv, _T("-p")) == 0)) { + Parents = 1; + } else if ((_tcscmp(*argv, _T("--")) == 0)) { + if (Debug) { + printOut(_T("-- detected, stop option parsing.\n")); + } + ++argv; + --argc; + break; + } else if ((_tcsncmp(*argv, _T("--"), 2) == 0) || (_tcsncmp(*argv, _T("-"), 1) == 0)) { + printErr(_T("Unknown option %s, use option --help for more information.\n"), *argv); + return eError; + } else { + break; + } + ++argv; + --argc; + } + + if (argc == 0) { + return printErr(_T("Too view arguments given.\n")); + } + + return runCommandForEachInputLine(argc, argv, mkdirOperation); +} + +tCommand CommandMkdir = { + _T("mkdir"), + printHelp, + runCommand, +}; diff --git a/src/OperationMkdir.cmake b/src/OperationMkdir.cmake new file mode 100644 index 0000000..505c932 --- /dev/null +++ b/src/OperationMkdir.cmake @@ -0,0 +1,60 @@ +test_fileop_command_common( + COMMAND mkdir + HELP_REGULAR_EXPRESSION "Create directories\\." "Available options are:" +) + +test_fileop_check_filesystem( + NAME Mkdir + ARGS mkdir mkdir + MUST_EXIST mkdir +) + +test_fileop_check_filesystem( + NAME MkdirExistingDir + ARGS mkdir mkdir + DEPENDS Mkdir + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Directory [A-Z]:\\\\.+\\\\mkdir already exists\\." +) + +test_fileop_check_filesystem( + NAME MkdirExistingDirParents + ARGS mkdir --parents mkdir + DEPENDS Mkdir +) + +test_fileop_check_filesystem( + NAME MkdirDirStructure + ARGS mkdir mkdir/a/b/c + DEPENDS MkdirExistingDirParents + WILL_FAIL + MUST_NOT_EXIST mkdir/a/b/c +) + +test_fileop_check_filesystem( + NAME MkdirDirStructureParents + ARGS mkdir --parents mkdir/a/b/c + DEPENDS MkdirDirStructure + MUST_EXIST mkdir/a/b/c +) + +test_fileop_check_filesystem( + NAME MkdirWithDashDash + ARGS mkdir -- --mkdir + MUST_EXIST --mkdir +) + +test_fileop_check_filesystem( + NAME MkdirFilename + ARGS mkdir -- ${CMAKE_CURRENT_LIST_FILE} + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Not a directory [A-Z]:\\\\.*\\\\OperationMkdir.cmake\\." +) + +test_fileop_check_filesystem( + NAME MkdirIllegalCharacter + ARGS mkdir --parents "test/invalid_|_name/test_" + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Not a directory [A-Z]:\\\\.*\\\\test\\\\invalid_|_name\\\\test_:" + MUST_EXIST test +) diff --git a/src/OperationMkdir.h b/src/OperationMkdir.h new file mode 100644 index 0000000..2afa6ba --- /dev/null +++ b/src/OperationMkdir.h @@ -0,0 +1,9 @@ + +#ifndef MKDIR_INCLUDED +#define MKDIR_INCLUDED + +#include "Types.h" + +extern tCommand CommandMkdir; + +#endif diff --git a/src/OperationMove.c b/src/OperationMove.c new file mode 100644 index 0000000..1cfd659 --- /dev/null +++ b/src/OperationMove.c @@ -0,0 +1,249 @@ + +#include "BasicFileOp.h" +#include "FileOp.h" +#include "Message.h" + +//! Global value for force. +static tBool Force = eFalse; +//! Global value for recursive. +static tBool Recursive = eFalse; +//! Check if there is no name collision before doing the action. +static tBool CheckUniqueNames = eFalse; +//! Global value for touching the files. +static tBool Touch = eFalse; + +static void printHelp(void) { + printOut(_T("Move files or directories.\n")); + printOut(_T("\n")); + printOut(_T("Available options are (Arguments for long options are also needed for short\n")); + printOut(_T("options):\n")); + printOut(_T(" -f, --force Ignore write protection of existing targets.\n")); + printOut(_T(" --touch Touch the files after copy.\n")); + printOut(_T(" --time=TIME Use local ISO time instead of current system time for.\n")); + printOut(_T(" the touch operation, e.g. 2021-01-31T15:05:01. If the\n")); + printOut(_T(" time is omitted, 12:00:00 is assumed.\n")); + printOut(_T(" -t, --target-directory=DIR Target directory to use. If not\n")); + printOut(_T(" given, the last argument is used as target dir.\n")); + printOut(_T(" --check-unique-names If multiple files or folders are copied check upfront\n")); + printOut(_T(" if there are any collisions. This isn't supported with\n")); + printOut(_T(" wildcards.\n")); + printOut(_T("\n")); + printOut(_T("The write protection is not copied to the target elements.\n")); + printOut(_T("If several sources are given or target is an existing directory the files\n")); + printOut(_T("are created with the original name inside target.\n")); + + return; +} + +static tResult moveOperation(void) { + static tBool RecursiveMoveNeeded = eFalse; + + DWORD dwAttrs, dwAttrsTarget; + int result = eOk; + LPTSTR StartOfTargetName = &TargetDosDevicePath[_tcslen(TargetDosDevicePath)]; + + // If target is a directory, the source name must be added and the attributes + // must be updated + dwAttrsTarget = GetFileAttributes(TargetDosDevicePath); + if (isDirectory(dwAttrsTarget)) { + _tcscpy(StartOfTargetName, _tcsrchr(DosDevicePath, _T('\\'))); + dwAttrsTarget = GetFileAttributes(TargetDosDevicePath); + } + + dwAttrs = GetFileAttributes(DosDevicePath); + if (Force && isReadonly(dwAttrsTarget)) { + result &= clearReadonly(TargetDosDevicePath, dwAttrsTarget); + } + + if (result == eOk) { + + if (isDirectory(dwAttrs)) { + if (RecursiveMoveNeeded == eFalse) { + if (Debug) { + printOut(_T("Try to move %s directly to %s, this only works on same device.\n"), getReadableFilename(DosDevicePath), + getReadableFilename(TargetDosDevicePath)); + } + if (MoveFile(DosDevicePath, TargetDosDevicePath) == 0) { + RecursiveMoveNeeded = eTrue; + if (Debug) { + printOut(_T("Direct move fails, fallback to recursive move.\n")); + } + } + } + + // No else because value can be changed inside if. + if (RecursiveMoveNeeded == eTrue) { + // If move fails, we create the directories in target and move each + // file. The empty source directory is removed at the end. + if (isDirectory(dwAttrsTarget) == eFalse) { + result &= createSingleDirectory(TargetDosDevicePath); // GCOVR_EXCL_LINE + } + if (result == eOk) { + HANDLE hFind; // search handle + WIN32_FIND_DATA FindFileData; + // File pattern for all files + _tcscat(DosDevicePath, _T("\\*")); + + // Get the find handle and the data of the first file. + hFind = FindFirstFile(DosDevicePath, &FindFileData); + // GCOVR_EXCL_START + if (hFind == INVALID_HANDLE_VALUE) { + result &= printLastError(_T("Got invalid handle for %s"), getReadableFilename(DosDevicePath)); + } + // GCOVR_EXCL_STOP + else { + LPTSTR StartOfSourceName; + // Save the position of the filename + StartOfSourceName = &DosDevicePath[_tcslen(DosDevicePath) - 1]; + + do { + if ((_tcscmp(FindFileData.cFileName, _T(".")) != 0) && (_tcscmp(FindFileData.cFileName, _T("..")) != 0)) { + _tcscpy(StartOfSourceName, FindFileData.cFileName); + // recursive move it + result &= moveOperation(); + } + } while ((result == eOk) && (FindNextFile(hFind, &FindFileData) != 0)); + StartOfSourceName[-1] = _T('\0'); + if (result == eOk) { + // We are at the end of the list + // GCOVR_EXCL_START + if (GetLastError() != ERROR_NO_MORE_FILES) { + result &= printLastError(_T("Can't get next file")); + } + // GCOVR_EXCL_STOP + } + + // close handle to file + if (!FindClose(hFind)) { + result &= printLastError(_T("Can't close file search handle."));// GCOVR_EXCL_LINE + } + removeEmptyDirectory(DosDevicePath); + } + } + } + } else { + result &= moveSingleFile(DosDevicePath, TargetDosDevicePath, Force); + if (result == eOk) { + dwAttrsTarget = GetFileAttributes(TargetDosDevicePath); + if (isReadonly(dwAttrsTarget)) { + result &= clearReadonly(TargetDosDevicePath, dwAttrsTarget); + } + if (Touch) { + result &= touchSingleFile(TargetDosDevicePath, eFalse); + } + } + } + } + + StartOfTargetName[0] = _T('\0'); + + return result; +} + +/*! + * Move a file or directory. + * + * @return + */ +static tResult runCommand(int argc, wchar_t *argv[]) { + tBool TargetIsDirectory = eFalse; + while (argc != 0) { + if ((_tcscmp(*argv, _T("--help")) == 0) || (_tcscmp(*argv, _T("-h")) == 0)) { + printHelp(); + return eOk; + } else if ((_tcscmp(*argv, _T("--force")) == 0) || (_tcscmp(*argv, _T("-f")) == 0)) { + Force = eTrue; + } else if (_tcscmp(*argv, _T("--touch")) == 0) { + Touch = eTrue; + } else if (isTimeOption(*argv)) { + if (storeTimeValue(&argc, &argv) != eOk) { + return eError; + } + } else if ((_tcscmp(*argv, _T("-t")) == 0) || (_tcsncmp(*argv, _T("--target-directory"), 18) == 0)) { + LPCTSTR Ptr; + if (_tcsncmp(*argv, _T("--target-directory="), 19) == 0) { + Ptr = &(*argv)[19]; + } else { + if (argc == 1) { + return printErr(_T("Option %s needs an argument.\n"), *argv); + } + Ptr = *(++argv); + --argc; + } + if (Ptr[0] == _T('\0')) { + return printErr(_T("Target directory must not be empty.\n"), *argv); + } + createDosDevicePath(Ptr, TargetDosDevicePath); + TargetIsDirectory = eTrue; + } else if (_tcscmp(*argv, _T("--check-unique-names")) == 0) { + CheckUniqueNames = eTrue; + } else if ((_tcscmp(*argv, _T("--")) == 0)) { + if (Debug) { + printOut(_T("-- detected, stop option parsing.\n")); + } + ++argv; + --argc; + break; + } else if ((_tcsncmp(*argv, _T("--"), 2) == 0) || (_tcsncmp(*argv, _T("-"), 1) == 0)) { + return printErr(_T("Unknown option %s, use option --help for more information.\n"), *argv); + } else { + break; + } + ++argv; + --argc; + } + + if (argc == 0) { + return printErr(_T("Too view arguments given.\n")); + } + + if (TargetDosDevicePath[0] == _T('\0')) { + createDosDevicePath(argv[--argc], TargetDosDevicePath); + if (TargetDosDevicePath == _T('\0')) { + return printErr(_T("Argument must not be empty.\n")); + } else { + if (argc == 0) { + return printErr(_T("Too view arguments given.\n")); + } else if (argc > 1) { + TargetIsDirectory = eTrue; + } + } + } + + unsigned int lastCharacterPos = _tcslen(TargetDosDevicePath) - 1; + while (TargetDosDevicePath[lastCharacterPos] == _T('\\')) { + TargetIsDirectory = eTrue; + TargetDosDevicePath[lastCharacterPos--] = _T('\0'); + } + + if (TargetIsDirectory) { + DWORD dwAttrsTarget = GetFileAttributes(TargetDosDevicePath); + if (isValidFileAttributes(dwAttrsTarget)) { + if (isDirectory(dwAttrsTarget) == eFalse) { + return printErr(_T("Target %s must be a directory."), getReadableFilename(TargetDosDevicePath)); + } else if (Force && isReadonly(dwAttrsTarget)) { + // GCOVR_EXCL_START + if (clearReadonly(TargetDosDevicePath, dwAttrsTarget) == 9) { + return eError; + } + // GCOVR_EXCL_STOP + } + } else { + return printErr(_T("Directory %s doesn't exist."), getReadableFilename(TargetDosDevicePath)); + } + } + + if (CheckUniqueNames) { + if (checkUniqueNames(argc, argv) == eError) { + return eError; + } + } + + return runCommandForEachInputLine(argc, argv, moveOperation); +} + +tCommand CommandMove = { + _T("move"), + printHelp, + runCommand, +}; diff --git a/src/OperationMove.cmake b/src/OperationMove.cmake new file mode 100644 index 0000000..6415224 --- /dev/null +++ b/src/OperationMove.cmake @@ -0,0 +1,154 @@ +test_fileop_command_common( + COMMAND move + HELP_REGULAR_EXPRESSION "Move files or directories\\." "Available options are:" + TEST_TIME_OPTION +) + +test_fileop_check_filesystem( + NAME MoveMissingTarget + ARGS move ${CMAKE_CURRENT_LIST_FILE} + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Too view arguments given\\." +) + +test_fileop_check_filesystem( + NAME MoveMissingSource + ARGS move file1 file2 + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Can't move file .+\\\\file to .+\\\\file2: The system cannot find the file specified\\." +) + +test_fileop_check_filesystem( + NAME MoveMissingTargetDirectoryI + ARGS move --target-directory= CopyMissingTargetDirectoryI + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Target directory must not be empty\\." +) + +test_fileop_check_filesystem( + NAME MoveMissingTargetDirectoryII + ARGS move --target-directory + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Option --target-directory needs an argument\\." +) + +test_fileop_check_filesystem( + NAME MoveNonExistingTargetDirectory + PREPARE_COMMAND "cp -f ${CMAKE_CURRENT_LIST_FILE} MoveNonExistingTargetDirectory.in" + ARGS move MoveNonExistingTargetDirectory.in MoveNonExistingTargetDirectory/ + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Directory [A-Z]:\\\\.+\\\\MoveNonExistingTargetDirectory doesn't exist\\." + MUST_NOT_EXIST MoveNonExistingTargetDirectory/MoveNonExistingTargetDirectory.in +) + +test_fileop_check_filesystem( + NAME MoveFileWithTargetDirectory + PREPARE_COMMAND "mkdir SubDir && cp -f ${CMAKE_CURRENT_LIST_FILE} ." + ARGS move --target-directory=SubDir OperationMove.cmake + MUST_NOT_EXIST OperationMove.cmake + MUST_EXIST SubDir/OperationMove.cmake +) + +test_fileop_check_filesystem( + NAME MoveFileWithTargetDirectoryForce + PREPARE_COMMAND "mkdir SubDir && cp -f ${CMAKE_CURRENT_LIST_FILE} ." + ARGS move --force --target-directory SubDir OperationMove.cmake + MUST_NOT_EXIST OperationMove.cmake + MUST_EXIST SubDir/OperationMove.cmake +) + +test_fileop_check_filesystem( + NAME MoveFileWithTargetDirectoryUniqueNames + PREPARE_COMMAND "mkdir SubDir additional_file && cp -f ${CMAKE_CURRENT_LIST_FILE} . && cp -f ${CMAKE_CURRENT_LIST_FILE} additional_file" + ARGS move --check-unique-names --target-directory SubDir ../../FileOp.exe OperationMove.cmake additional_file/OperationMove.cmake + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: File in source list will overwrite each other: OperationMove.cmake" + MUST_EXIST OperationMove.cmake additional_file/OperationMove.cmake + MUST_NOT_EXIST SubDir/OperationMove.cmake +) + +test_fileop_check_filesystem( + NAME MoveFileWithPattern + PREPARE_COMMAND "mkdir SubDir && cp -f ${CMAKE_CURRENT_LIST_DIR}/*.* ." + ARGS move --target-directory SubDir OperationMove.* + MUST_EXIST SubDir/OperationMove.c SubDir/OperationMove.cmake SubDir/OperationMove.h + MUST_NOT_EXIST OperationMove.c OperationMove.cmake OperationMove.h +) + +test_fileop_check_filesystem( + NAME MoveFileListWithPattern + PREPARE_COMMAND "mkdir -p Source Target && cp -f ${CMAKE_CURRENT_LIST_DIR}/OperationMove.* Source/" + ARGS move --touch --time=2001-01-01T00:30 Source/OperationMove.* Target/ + MUST_EXIST Target/OperationMove.c Target/OperationMove.cmake Target/OperationMove.h + MUST_NOT_EXIST Source/OperationMove.c Source/OperationMove.cmake Source/OperationMove.h + FILE_TIMESTAMP_REGEX "2001-01-01 00:30:00\\.000000000 \\+0000" +) + +test_fileop_check_filesystem( + NAME MoveFileListWithPatternExisting + PREPARE_COMMAND "mkdir -p Source Target && cp -f ${CMAKE_CURRENT_LIST_DIR}/OperationMove.* Source/ && cp -f Source/*.* Target/" + ARGS move --touch --time=2002-01-01T00:30 Target/OperationMove.* MoveFileListWithPatternTarget/ + WILL_FAIL + MUST_EXIST Source/OperationMove.c Source/OperationMove.cmake Source/OperationMove.h +) + +test_fileop_check_filesystem( + NAME MoveFileListWithPatternExistingForced + PREPARE_COMMAND "mkdir -p Source Target && cp -f ${CMAKE_CURRENT_LIST_DIR}/OperationMove.* Source/ && cp -f Source/*.* Target/ && chmod -R oga-w ." + ARGS move --touch --force --time=2002-01-01T00:30 Source/*.* Target/ + MUST_EXIST Target/OperationMove.c Target/OperationMove.cmake Target/OperationMove.h + MUST_NOT_EXIST Source/OperationMove.c Source/OperationMove.cmake Source/OperationMove.h + FILE_TIMESTAMP_REGEX "2002-01-01 00:30:00\\.000000000 \\+0000" +) + +test_fileop_check_filesystem( + NAME MoveFileWithDashDash + PREPARE_COMMAND "cp -f -- ${CMAKE_CURRENT_LIST_FILE} --source" + ARGS move -- --source --target + MUST_NOT_EXIST --source + MUST_EXIST --target +) + +test_fileop_check_filesystem( + NAME MoveFileToDirectory + PREPARE_COMMAND "cp -f -- ${CMAKE_CURRENT_LIST_FILE} --source && mkdir -p SubDir" + ARGS move -- --source SubDir + MUST_NOT_EXIST --source + MUST_EXIST SubDir/--source +) + +test_fileop_check_filesystem( + NAME MoveDirectoryToExistingFile + PREPARE_COMMAND "mkdir SubDir && touch target SubDir/file" + ARGS --debug move --target-directory target SubDir + WILL_FAIL + MUST_EXIST target + MUST_NOT_EXIST target/SubDir/file + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Target .+\\\\target must be a directory\\." +) + +test_fileop_check_filesystem( + NAME MoveFileExistingTarget + PREPARE_COMMAND "touch source target" + ARGS move -- source target + WILL_FAIL + MUST_EXIST source target + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Can't move file .+\\\\source to .+\\\\target: The file exists\\." +) + +test_fileop_check_filesystem( + NAME MoveFileExistingTargetForce + PREPARE_COMMAND "touch source target && chmod -R oga-w ." + ARGS move --force -- source target + MUST_NOT_EXIST source + MUST_EXIST target +) + +test_fileop_check_filesystem( + NAME MoveDirectoryRecursive + PREPARE_COMMAND "mkdir -p source target/source && cp -rf ${CMAKE_CURRENT_LIST_DIR}/OperationMove.* source/ && chmod -R oga-w ." + ARGS --debug move --force --touch --time 2000-01-01 source target + MUST_EXIST target/source/OperationMove.c target/source/OperationMove.cmake target/source/OperationMove.h + MUST_NOT_EXIST source/OperationMove.c source/OperationMove.cmake source/OperationMove.h + FILE_TIMESTAMP_REGEX "2000-01-01 12:00:00\\.000000000 \\+0000" +) diff --git a/src/OperationMove.h b/src/OperationMove.h new file mode 100644 index 0000000..9816300 --- /dev/null +++ b/src/OperationMove.h @@ -0,0 +1,9 @@ + +#ifndef MOVE_INCLUDED +#define MOVE_INCLUDED + +#include "Types.h" + +extern tCommand CommandMove; + +#endif diff --git a/src/OperationRemove.c b/src/OperationRemove.c new file mode 100644 index 0000000..ef5f165 --- /dev/null +++ b/src/OperationRemove.c @@ -0,0 +1,138 @@ + +#include "BasicFileOp.h" +#include "FileOp.h" +#include "Message.h" + +//! Global value for force. +static tBool Force = eFalse; +//! Global value for recursive. +static tBool Recursive = eFalse; + +static void printHelp(void) { + printOut(_T("Remove files or directories.\n")); + printOut(_T("\n")); + printOut(_T("Available options are:\n")); + printOut(_T(" -r, -R, --recursive Remove files recursive.\n")); + printOut(_T(" -f, --force Ignore write protection.\n")); + printOut(_T("\n")); + printOut(_T("Examples:\n")); + printOut(_T("- Delete two folders recursive, ignoring write protection:\n")); + printOut(_T(" %s remove --recursive --force c:\\temp\\subFolder c:\\temp\\aFile.txt\n"), ProgramName); + + return; +} + +static tResult removeOperation(void) { + int result = eOk; + DWORD dwAttrs = GetFileAttributes(DosDevicePath); + if (isValidFileAttributes(dwAttrs)) { + if (Force && isReadonly(dwAttrs)) { + result &= clearReadonly(DosDevicePath, dwAttrs); + } + + if (result == eOk) { + if (isReparsePoint(dwAttrs)) { + result &= removeReparsePoint(DosDevicePath); + } else if (isDirectory(dwAttrs)) { + if (Recursive) { + HANDLE hFind; // search handle + WIN32_FIND_DATA FindFileData; + // File pattern for all files + _tcscat(DosDevicePath, _T("\\*")); + + // Get the find handle and the data of the first file. + hFind = FindFirstFile(DosDevicePath, &FindFileData); + // GCOVR_EXCL_START + if (hFind == INVALID_HANDLE_VALUE) { + result &= printLastError(_T("Got invalid handle for %s"), getReadableFilename(DosDevicePath)); + } + // GCOVR_EXCL_STOP + else { + LPTSTR StartOfName; + // Save the position of the filename + StartOfName = &DosDevicePath[_tcslen(DosDevicePath) - 1]; + + do { + if ((_tcscmp(FindFileData.cFileName, _T(".")) != 0) && (_tcscmp(FindFileData.cFileName, _T("..")) != 0)) { + _tcscpy(StartOfName, FindFileData.cFileName); + // recursive remove it + result &= removeOperation(); + } + } while ((result == eOk) && (FindNextFile(hFind, &FindFileData) != 0)); + if (result == eOk) { + // GCOVR_EXCL_START + if (GetLastError() != ERROR_NO_MORE_FILES) { + result &= printLastError(_T("Can't get next file")); + } + // GCOVR_EXCL_STOP + } + + StartOfName[-1] = _T('\0'); + // close handle to file + if (!FindClose(hFind)) { + result &= printLastError(_T("Can't close file search handle.")); // GCOVR_EXCL_LINE + } + } + } + + if (result == eOk) { + // Remove the empty directory + result &= removeEmptyDirectory(DosDevicePath); + } + } else { + result &= removeSingleFile(DosDevicePath); + } + } + } else if (Debug) { + printOut(_T("Skip %s because it doesn't exist.\n"), getReadableFilename(DosDevicePath)); + } + + return result; +} + +/*! + * Remove a file or directory. + * + * If global variable Recurse is set all sub directories are also removed. + * If global variable Force is set the write protection is removed before + * deleting the elements. + * + * @return eOk on success, else eError. + */ +static tResult runCommand(int argc, wchar_t *argv[]) { + while (argc != 0) { + if ((_tcscmp(*argv, _T("--help")) == 0) || (_tcscmp(*argv, _T("-h")) == 0)) { + printHelp(); + return eOk; + } else if ((_tcscmp(*argv, _T("--force")) == 0) || (_tcscmp(*argv, _T("-f")) == 0)) { + Force = 1; + } else if ((_tcscmp(*argv, _T("--recursive")) == 0) || (_tcscmp(*argv, _T("-r")) == 0) || (_tcscmp(*argv, _T("-R")) == 0)) { + Recursive = 1; + } else if ((_tcscmp(*argv, _T("--")) == 0)) { + if (Debug) { + printOut(_T("-- detected, stop option parsing.\n")); + } + ++argv; + --argc; + break; + } else if ((_tcsncmp(*argv, _T("--"), 2) == 0) || (_tcsncmp(*argv, _T("-"), 1) == 0)) { + return printErr(_T("Unknown option %s, use option --help for more information.\n"), *argv); + } else { + break; + } + ++argv; + --argc; + } + + if (argc == 0) { + return printErr(_T("Too view arguments given.\n")); + } + + return runCommandForEachInputLine(argc, argv, removeOperation); +} + +tCommand CommandRemove = { + _T("remove"), + printHelp, + runCommand, +}; diff --git a/src/OperationRemove.cmake b/src/OperationRemove.cmake new file mode 100644 index 0000000..3ba2dcc --- /dev/null +++ b/src/OperationRemove.cmake @@ -0,0 +1,53 @@ +test_fileop_command_common( + COMMAND remove + HELP_REGULAR_EXPRESSION "Remove files or directories\\." "Available options are:" +) + +test_fileop_check_filesystem( + NAME RemoveNonExisting + ARGS --debug remove file + PASS_REGULAR_EXPRESSION "Skip .+\\\\file because it doesn't exist\\." +) + +test_fileop_check_filesystem( + NAME RemoveSingleFile + PREPARE_COMMAND "touch file" + ARGS remove file + MUST_NOT_EXIST file +) + +set(REMOVE_PREPARE_COMMAND "mkdir -p test/subdir junction_target && echo 'Junction target file content' > junction_target/test_file && touch test/subdir/test_file_1 test/subdir/test_file_2_readonly test/subdir/test_file_3 && attrib +R test/subdir/test_file_2_readonly && pushd test/subdir && cmd.exe //C \"mklink /J junction ..\\..\\junction_target\" && cat junction/test_file && popd") + +test_fileop_check_filesystem( + NAME RemoveNotEmptyDirectory + PREPARE_COMMAND ${REMOVE_PREPARE_COMMAND} + ARGS remove test + WILL_FAIL + MUST_EXIST junction_target/test_file test/subdir/junction/test_file test/subdir/test_file_1 test/subdir/test_file_3 + FAIL_REGULAR_EXPRESSION "FileOp.exe: error: Can't remove directory .+\\\\test: The directory is not empty." +) + +test_fileop_check_filesystem( + NAME RemoveNotEmptyDirectoryRecursive + PREPARE_COMMAND ${REMOVE_PREPARE_COMMAND} + ARGS --debug remove --recursive test + WILL_FAIL + MUST_EXIST junction_target/test_file test/subdir/test_file_3 + MUST_NOT_EXIST test/subdir/junction/test_file test/subdir/test_file_1 + FAIL_REGULAR_EXPRESSION "FileOp.exe: error: Can't remove file .+\\\\test\\\\subdir\\\\test_file_2_readonly: Access is denied." +) + +test_fileop_check_filesystem( + NAME RemoveNotEmptyDirectoryRecursiveForce + PREPARE_COMMAND ${REMOVE_PREPARE_COMMAND} + ARGS remove --recursive --force test + MUST_EXIST junction_target/test_file + MUST_NOT_EXIST test/subdir/junction/test_file test/subdir/test_file_1 test/subdir/test_file_3 +) + +test_fileop_check_filesystem( + NAME RemoveFileWithDashDash + PREPARE_COMMAND "touch -- --file" + ARGS remove -- --file + MUST_NOT_EXIST --file +) diff --git a/src/OperationRemove.h b/src/OperationRemove.h new file mode 100644 index 0000000..a81ea16 --- /dev/null +++ b/src/OperationRemove.h @@ -0,0 +1,8 @@ + +#ifndef REMOVE_INCLUDED +#define REMOVE_INCLUDED +#include "Types.h" + +extern tCommand CommandRemove; + +#endif diff --git a/src/OperationTouch.c b/src/OperationTouch.c new file mode 100644 index 0000000..e955b42 --- /dev/null +++ b/src/OperationTouch.c @@ -0,0 +1,64 @@ + +#include "BasicFileOp.h" +#include "FileOp.h" +#include "Message.h" +#include "Types.h" + +static void printHelp(void) { + printOut(_T("Touch the given files. This means creating it or updating the timestamp if it\n")); + printOut(_T("already exists.\n")); + printOut(_T("If the file starts with an @, the files listed in the files are touched.\n")); + printOut(_T("\n")); + printOut(_T("Available options are:\n")); + printOut(_T(" --time=TIME Use local ISO time instead of current system time.\n")); + printOut(_T(" E.g. 2021-01-31T15:05:01 if no time is given, 12:00:00 is\n")); + printOut(_T(" assumed.\n")); + + return; +} + +static tResult touchOperation(void) { return touchSingleFile(DosDevicePath, eTrue); } + +/*! + * Print a file to STDOUT. + * + * @return eOk on success, else eError. + */ +static tResult runCommand(int argc, wchar_t *argv[]) { + + while (argc != 0) { + if ((_tcscmp(*argv, _T("--help")) == 0) || (_tcscmp(*argv, _T("-h")) == 0)) { + printHelp(); + return eOk; + } else if (isTimeOption(*argv)) { + if (storeTimeValue(&argc, &argv) != eOk) { + return eError; + } + } else if ((_tcscmp(*argv, _T("--")) == 0)) { + if (Debug) { + printOut(_T("-- detected, stop option parsing.\n")); + } + ++argv; + --argc; + break; + } else if ((_tcsncmp(*argv, _T("--"), 2) == 0) || (_tcsncmp(*argv, _T("-"), 1) == 0)) { + return printErr(_T("Unknown option %s, use option --help for more information.\n"), *argv); + } else { + break; + } + ++argv; + --argc; + } + + if (argc == 0) { + return printErr(_T("Too view arguments given.\n")); + } + + return runCommandForEachInputLine(argc, argv, touchOperation); +} + +tCommand CommandTouch = { + _T("touch"), + printHelp, + runCommand, +}; diff --git a/src/OperationTouch.cmake b/src/OperationTouch.cmake new file mode 100644 index 0000000..8d0fa6f --- /dev/null +++ b/src/OperationTouch.cmake @@ -0,0 +1,46 @@ +test_fileop_command_common( + COMMAND touch + HELP_REGULAR_EXPRESSION "Touch the given files\\. This means creating it or updating the timestamp of it" "already exists\\." "Available options are:" + TEST_TIME_OPTION +) + +test_fileop_check_filesystem( + NAME TouchFileSpaceTimestamp + ARGS --debug touch --time 2000-01-01T12:30 TouchFileSpaceTimestamp + MUST_EXIST TouchFileSpaceTimestamp + FILE_TIMESTAMP_REGEX "2000-01-01 12:30:00\\.000000000 \\+0000" +) + +test_fileop_check_filesystem( + NAME TouchFileEqualTimestamp + ARGS --debug touch --time 2001-01-02T00:30 TouchFileEqualTimestamp + MUST_EXIST TouchFileEqualTimestamp + FILE_TIMESTAMP_REGEX "2001-01-02 00:30:00\\.000000000 \\+0000" +) + +test_fileop_check_filesystem( + NAME TouchFileLeadingDashDash + ARGS --debug touch -- --xxx + MUST_EXIST --xxx +) + +test_fileop_check_filesystem( + NAME TouchFileLeadingDosDeviceName + ARGS --debug touch //?/${CMAKE_CURRENT_BINARY_DIR}/TouchFileLeadingDosDeviceName/file + MUST_EXIST file +) + +test_fileop( + NAME TouchFileUnc + ARGS --debug touch //machine/share/file + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Can't get handle to UNC\\\\machine\\\\share\\\\file: The network (name cannot be|path was not) found\\." + +) + +test_fileop( + NAME TouchFileLeadingDosDeviceNameUnc + ARGS --debug touch //?/UNC/machine/share/file + WILL_FAIL + FAIL_REGULAR_EXPRESSION "FileOp\\.exe: error: Can't get handle to UNC\\\\machine\\\\share\\\\file: The network (name cannot be|path was not) found\\." +) diff --git a/src/OperationTouch.h b/src/OperationTouch.h new file mode 100644 index 0000000..ec1ce63 --- /dev/null +++ b/src/OperationTouch.h @@ -0,0 +1,9 @@ + +#ifndef TOUCH_INCLUDED +#define TOUCH_INCLUDED + +#include "Types.h" + +extern tCommand CommandTouch; + +#endif diff --git a/src/Types.h b/src/Types.h new file mode 100644 index 0000000..d575421 --- /dev/null +++ b/src/Types.h @@ -0,0 +1,35 @@ + +#ifndef TYPES_INCLUDES +#define TYPES_INCLUDES + +#define _UNICODE // use Unicode (Microsoft C run-time API) +#define UNICODE // use Unicode (Microsoft Windows API) +#define _WIN32_WINNT _WIN32_WINNT_WINXP + +#include +#include +#include + +//! Local result type. +typedef enum { + //! The local false value. + eError = 0, + //! The local true value. + eOk = 1 +} tResult; + +//! Local boolean type. +typedef enum { + //! The local false value. + eFalse = 0, + //! The local true value. + eTrue = 1 +} tBool; + +typedef struct { + const TCHAR id[10]; + void (*printHelp)(void); + tResult (*run)(int argc, wchar_t *argv[]); +} tCommand; + +#endif diff --git a/src/mingw-unicode.c b/src/mingw-unicode.c new file mode 100644 index 0000000..e2df054 --- /dev/null +++ b/src/mingw-unicode.c @@ -0,0 +1,57 @@ +// This document is released into the public domain. Absolutely no warranty is provided. +// See http://www.coderforlife.com/projects/utilities. +// +// This is for the MinGW compiler which does not support wmain. +// It is a wrapper for _tmain when _UNICODE is defined (wmain). +// +// !! Do not compile this file, but instead include it right before your _tmain function like: +// #include "mingw-unicode.c" +// int _tmain(int argc, _TCHAR *argv[]) { +// +// If you wish to have enpv in your main, then define the following before including this file: +// #define MAIN_USE_ENVP +// +// This wrapper adds ~300 bytes to the program and negligible overhead + +#undef _tmain +#ifdef _UNICODE +#define _tmain wmain +#else +#define _tmain main +#endif + +#if defined(__GNUC__) && defined(_UNICODE) + +#ifndef __MSVCRT__ +#error Unicode main function requires linking to MSVCRT +#endif + +#include +#include + +extern int _CRT_glob; +extern +#ifdef __cplusplus + "C" +#endif + void + __wgetmainargs(int *, wchar_t ***, wchar_t ***, int, int *); + +#ifdef MAIN_USE_ENVP +int wmain(int argc, wchar_t *argv[], wchar_t *envp[]); +#else +int wmain(int argc, wchar_t *argv[]); +#endif + +int main() { + wchar_t **enpv, **argv; + int argc, si = 0; + __wgetmainargs(&argc, &argv, &enpv, _CRT_glob, &si); // this also creates the global variable __wargv +#ifdef MAIN_USE_ENVP + return wmain(argc, argv, enpv); +#else + return wmain(argc, argv); +#endif +} + +#endif // defined(__GNUC__) && defined(_UNICODE)