From c73c92688074454edf541675795b719751f5d912 Mon Sep 17 00:00:00 2001 From: Uwe Seimet Date: Wed, 29 Nov 2023 17:32:53 +0100 Subject: [PATCH] Initial codebase --- .github/workflows/arm_cross_compile.yml | 62 ++ .github/workflows/build_code.yml | 15 + .github/workflows/cpp.yml | 107 +++ .github/workflows/macos_compile.yml | 34 + LICENSE | 33 + README.md | 36 + cpp/Makefile | 377 ++++++++ cpp/base/device.cpp | 138 +++ cpp/base/device.h | 145 +++ cpp/base/device_factory.cpp | 155 ++++ cpp/base/device_factory.h | 42 + cpp/base/device_logger.cpp | 61 ++ cpp/base/device_logger.h | 43 + cpp/base/interfaces/scsi_block_commands.h | 29 + cpp/base/interfaces/scsi_mmc_commands.h | 22 + cpp/base/interfaces/scsi_primary_commands.h | 31 + cpp/base/interfaces/scsi_printer_commands.h | 26 + cpp/base/primary_device.cpp | 312 +++++++ cpp/base/primary_device.h | 113 +++ cpp/base/scsi_command_util.cpp | 139 +++ cpp/base/scsi_command_util.h | 59 ++ cpp/buses/bus.cpp | 84 ++ cpp/buses/bus.h | 99 ++ cpp/buses/bus_factory.cpp | 66 ++ cpp/buses/bus_factory.h | 38 + cpp/buses/connection_type/connection_aibom.h | 55 ++ .../connection_type/connection_fullspec.h | 55 ++ .../connection_type/connection_gamernium.h | 55 ++ .../connection_type/connection_standard.h | 55 ++ cpp/buses/gpiobus.cpp | 298 ++++++ cpp/buses/gpiobus.h | 191 ++++ cpp/buses/gpiobus_raspberry.cpp | 869 ++++++++++++++++++ cpp/buses/gpiobus_raspberry.h | 214 +++++ cpp/buses/in_process_bus.cpp | 96 ++ cpp/buses/in_process_bus.h | 256 ++++++ cpp/buses/pin_control.h | 61 ++ cpp/command_interface.proto | 478 ++++++++++ cpp/controllers/abstract_controller.cpp | 139 +++ cpp/controllers/abstract_controller.h | 150 +++ cpp/controllers/controller_factory.cpp | 120 +++ cpp/controllers/controller_factory.h | 53 ++ cpp/controllers/generic_controller.cpp | 743 +++++++++++++++ cpp/controllers/generic_controller.h | 67 ++ cpp/controllers/phase_handler.cpp | 21 + cpp/controllers/phase_handler.h | 68 ++ cpp/controllers/sasi_controller.cpp | 41 + cpp/controllers/sasi_controller.h | 32 + cpp/controllers/scsi_controller.cpp | 182 ++++ cpp/controllers/scsi_controller.h | 70 ++ cpp/devices/cd_dvd.cpp | 364 ++++++++ cpp/devices/cd_dvd.h | 64 ++ cpp/devices/cd_track.cpp | 122 +++ cpp/devices/cd_track.h | 46 + cpp/devices/ctapdriver.cpp | 377 ++++++++ cpp/devices/ctapdriver.h | 71 ++ cpp/devices/daynaport.cpp | 431 +++++++++ cpp/devices/daynaport.h | 128 +++ cpp/devices/disk.cpp | 748 +++++++++++++++ cpp/devices/disk.h | 126 +++ cpp/devices/disk_cache.cpp | 220 +++++ cpp/devices/disk_cache.h | 74 ++ cpp/devices/disk_track.cpp | 279 ++++++ cpp/devices/disk_track.h | 57 ++ cpp/devices/host_services.cpp | 348 +++++++ cpp/devices/host_services.h | 87 ++ cpp/devices/mode_page_device.cpp | 139 +++ cpp/devices/mode_page_device.h | 50 + cpp/devices/optical_memory.cpp | 202 ++++ cpp/devices/optical_memory.h | 47 + cpp/devices/printer.cpp | 234 +++++ cpp/devices/printer.h | 67 ++ cpp/devices/sasi_hd.cpp | 64 ++ cpp/devices/sasi_hd.h | 29 + cpp/devices/scsi_hd.cpp | 99 ++ cpp/devices/scsi_hd.h | 43 + cpp/devices/storage_device.cpp | 118 +++ cpp/devices/storage_device.h | 73 ++ cpp/doc/s2p.1 | 114 +++ cpp/doc/s2p_man_page.txt | 149 +++ cpp/doc/s2pctl.1 | 195 ++++ cpp/doc/s2pctl_man_page.txt | 183 ++++ cpp/doc/s2pdump.1 | 71 ++ cpp/doc/s2pdump_man_page.txt | 81 ++ cpp/doc/s2pexec.1 | 57 ++ cpp/doc/s2pexec_man_page.txt | 104 +++ cpp/s2p/command_context.cpp | 104 +++ cpp/s2p/command_context.h | 52 ++ cpp/s2p/command_dispatcher.cpp | 281 ++++++ cpp/s2p/command_dispatcher.h | 45 + cpp/s2p/localizer.cpp | 287 ++++++ cpp/s2p/localizer.h | 77 ++ cpp/s2p/s2p.cpp | 18 + cpp/s2p/s2p_core.cpp | 471 ++++++++++ cpp/s2p/s2p_core.h | 78 ++ cpp/s2p/s2p_executor.cpp | 632 +++++++++++++ cpp/s2p/s2p_executor.h | 79 ++ cpp/s2p/s2p_image.cpp | 428 +++++++++ cpp/s2p/s2p_image.h | 53 ++ cpp/s2p/s2p_response.cpp | 545 +++++++++++ cpp/s2p/s2p_response.h | 65 ++ cpp/s2p/s2p_service.cpp | 114 +++ cpp/s2p/s2p_service.h | 47 + cpp/s2pctl/s2pctl.cpp | 18 + cpp/s2pctl/s2pctl_commands.cpp | 442 +++++++++ cpp/s2pctl/s2pctl_commands.h | 70 ++ cpp/s2pctl/s2pctl_core.h | 28 + cpp/s2pctl/s2pctl_display.cpp | 425 +++++++++ cpp/s2pctl/s2pctl_display.h | 45 + cpp/s2pctl/s2pctl_parser.cpp | 29 + cpp/s2pctl/s2pctl_parser.h | 48 + cpp/s2pctl/sp2ctl_core.cpp | 333 +++++++ cpp/s2pdump/s2pdump.cpp | 18 + cpp/s2pdump/s2pdump_core.cpp | 558 +++++++++++ cpp/s2pdump/s2pdump_core.h | 124 +++ cpp/s2pdump/s2pdump_executor.cpp | 128 +++ cpp/s2pdump/s2pdump_executor.h | 47 + cpp/s2pexec/s2pexec.cpp | 18 + cpp/s2pexec/s2pexec_core.cpp | 293 ++++++ cpp/s2pexec/s2pexec_core.h | 62 ++ cpp/s2pexec/s2pexec_executor.cpp | 117 +++ cpp/s2pexec/s2pexec_executor.h | 53 ++ cpp/shared/network_util.cpp | 74 ++ cpp/shared/network_util.h | 23 + cpp/shared/phase_executor.cpp | 272 ++++++ cpp/shared/phase_executor.h | 96 ++ cpp/shared/protobuf_util.cpp | 237 +++++ cpp/shared/protobuf_util.h | 49 + cpp/shared/s2p_util.cpp | 134 +++ cpp/shared/s2p_util.h | 56 ++ cpp/shared/s2p_version.h | 16 + cpp/shared/scsi.h | 194 ++++ cpp/shared/shared_exceptions.h | 44 + cpp/test/abstract_controller_test.cpp | 189 ++++ cpp/test/bus_test.cpp | 128 +++ cpp/test/cd_dvd_test.cpp | 134 +++ cpp/test/command_context_test.cpp | 150 +++ cpp/test/controller_manager_test.cpp | 86 ++ cpp/test/ctapdriver_test.cpp | 41 + cpp/test/daynaport_test.cpp | 186 ++++ cpp/test/device_factory_test.cpp | 221 +++++ cpp/test/device_test.cpp | 270 ++++++ cpp/test/disk_test.cpp | 825 +++++++++++++++++ cpp/test/host_services_test.cpp | 206 +++++ cpp/test/in_process_test.cpp | 74 ++ cpp/test/localizer_test.cpp | 32 + cpp/test/mocks.h | 408 ++++++++ cpp/test/mode_page_device_test.cpp | 154 ++++ cpp/test/network_util_test.cpp | 34 + cpp/test/optical_memory_test.cpp | 150 +++ cpp/test/phase_handler_test.cpp | 139 +++ cpp/test/primary_device_test.cpp | 356 +++++++ cpp/test/printer_test.cpp | 122 +++ cpp/test/protobuf_util_test.cpp | 240 +++++ cpp/test/s2p_executor_test.cpp | 616 +++++++++++++ cpp/test/s2p_image_test.cpp | 162 ++++ cpp/test/s2p_response_test.cpp | 258 ++++++ cpp/test/s2p_service_test.cpp | 109 +++ cpp/test/s2p_util_test.cpp | 138 +++ cpp/test/s2pctl_commands_test.cpp | 110 +++ cpp/test/s2pctl_display_test.cpp | 292 ++++++ cpp/test/s2pctl_parser_test.cpp | 50 + cpp/test/s2pdump_test.cpp | 71 ++ cpp/test/scsi_command_util_test.cpp | 214 +++++ cpp/test/scsi_controller_test.cpp | 282 ++++++ cpp/test/scsi_hd_test.cpp | 126 +++ cpp/test/shared_exceptions_test.cpp | 52 ++ cpp/test/storage_device_test.cpp | 162 ++++ cpp/test/test_setup.cpp | 35 + cpp/test/test_shared.cpp | 127 +++ cpp/test/test_shared.h | 45 + ide_setup/README | 5 + ide_setup/eclipse_code_formatter.xml | 193 ++++ 172 files changed, 26915 insertions(+) create mode 100644 .github/workflows/arm_cross_compile.yml create mode 100644 .github/workflows/build_code.yml create mode 100644 .github/workflows/cpp.yml create mode 100644 .github/workflows/macos_compile.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cpp/Makefile create mode 100644 cpp/base/device.cpp create mode 100644 cpp/base/device.h create mode 100644 cpp/base/device_factory.cpp create mode 100644 cpp/base/device_factory.h create mode 100644 cpp/base/device_logger.cpp create mode 100644 cpp/base/device_logger.h create mode 100644 cpp/base/interfaces/scsi_block_commands.h create mode 100644 cpp/base/interfaces/scsi_mmc_commands.h create mode 100644 cpp/base/interfaces/scsi_primary_commands.h create mode 100644 cpp/base/interfaces/scsi_printer_commands.h create mode 100644 cpp/base/primary_device.cpp create mode 100644 cpp/base/primary_device.h create mode 100644 cpp/base/scsi_command_util.cpp create mode 100644 cpp/base/scsi_command_util.h create mode 100644 cpp/buses/bus.cpp create mode 100644 cpp/buses/bus.h create mode 100644 cpp/buses/bus_factory.cpp create mode 100644 cpp/buses/bus_factory.h create mode 100644 cpp/buses/connection_type/connection_aibom.h create mode 100644 cpp/buses/connection_type/connection_fullspec.h create mode 100644 cpp/buses/connection_type/connection_gamernium.h create mode 100644 cpp/buses/connection_type/connection_standard.h create mode 100644 cpp/buses/gpiobus.cpp create mode 100644 cpp/buses/gpiobus.h create mode 100644 cpp/buses/gpiobus_raspberry.cpp create mode 100644 cpp/buses/gpiobus_raspberry.h create mode 100644 cpp/buses/in_process_bus.cpp create mode 100644 cpp/buses/in_process_bus.h create mode 100644 cpp/buses/pin_control.h create mode 100644 cpp/command_interface.proto create mode 100644 cpp/controllers/abstract_controller.cpp create mode 100644 cpp/controllers/abstract_controller.h create mode 100644 cpp/controllers/controller_factory.cpp create mode 100644 cpp/controllers/controller_factory.h create mode 100644 cpp/controllers/generic_controller.cpp create mode 100644 cpp/controllers/generic_controller.h create mode 100644 cpp/controllers/phase_handler.cpp create mode 100644 cpp/controllers/phase_handler.h create mode 100644 cpp/controllers/sasi_controller.cpp create mode 100644 cpp/controllers/sasi_controller.h create mode 100644 cpp/controllers/scsi_controller.cpp create mode 100644 cpp/controllers/scsi_controller.h create mode 100644 cpp/devices/cd_dvd.cpp create mode 100644 cpp/devices/cd_dvd.h create mode 100644 cpp/devices/cd_track.cpp create mode 100644 cpp/devices/cd_track.h create mode 100644 cpp/devices/ctapdriver.cpp create mode 100644 cpp/devices/ctapdriver.h create mode 100644 cpp/devices/daynaport.cpp create mode 100644 cpp/devices/daynaport.h create mode 100644 cpp/devices/disk.cpp create mode 100644 cpp/devices/disk.h create mode 100644 cpp/devices/disk_cache.cpp create mode 100644 cpp/devices/disk_cache.h create mode 100644 cpp/devices/disk_track.cpp create mode 100644 cpp/devices/disk_track.h create mode 100644 cpp/devices/host_services.cpp create mode 100644 cpp/devices/host_services.h create mode 100644 cpp/devices/mode_page_device.cpp create mode 100644 cpp/devices/mode_page_device.h create mode 100644 cpp/devices/optical_memory.cpp create mode 100644 cpp/devices/optical_memory.h create mode 100644 cpp/devices/printer.cpp create mode 100644 cpp/devices/printer.h create mode 100644 cpp/devices/sasi_hd.cpp create mode 100644 cpp/devices/sasi_hd.h create mode 100644 cpp/devices/scsi_hd.cpp create mode 100644 cpp/devices/scsi_hd.h create mode 100644 cpp/devices/storage_device.cpp create mode 100644 cpp/devices/storage_device.h create mode 100644 cpp/doc/s2p.1 create mode 100644 cpp/doc/s2p_man_page.txt create mode 100644 cpp/doc/s2pctl.1 create mode 100644 cpp/doc/s2pctl_man_page.txt create mode 100644 cpp/doc/s2pdump.1 create mode 100644 cpp/doc/s2pdump_man_page.txt create mode 100644 cpp/doc/s2pexec.1 create mode 100644 cpp/doc/s2pexec_man_page.txt create mode 100644 cpp/s2p/command_context.cpp create mode 100644 cpp/s2p/command_context.h create mode 100644 cpp/s2p/command_dispatcher.cpp create mode 100644 cpp/s2p/command_dispatcher.h create mode 100644 cpp/s2p/localizer.cpp create mode 100644 cpp/s2p/localizer.h create mode 100644 cpp/s2p/s2p.cpp create mode 100644 cpp/s2p/s2p_core.cpp create mode 100644 cpp/s2p/s2p_core.h create mode 100644 cpp/s2p/s2p_executor.cpp create mode 100644 cpp/s2p/s2p_executor.h create mode 100644 cpp/s2p/s2p_image.cpp create mode 100644 cpp/s2p/s2p_image.h create mode 100644 cpp/s2p/s2p_response.cpp create mode 100644 cpp/s2p/s2p_response.h create mode 100644 cpp/s2p/s2p_service.cpp create mode 100644 cpp/s2p/s2p_service.h create mode 100644 cpp/s2pctl/s2pctl.cpp create mode 100644 cpp/s2pctl/s2pctl_commands.cpp create mode 100644 cpp/s2pctl/s2pctl_commands.h create mode 100644 cpp/s2pctl/s2pctl_core.h create mode 100644 cpp/s2pctl/s2pctl_display.cpp create mode 100644 cpp/s2pctl/s2pctl_display.h create mode 100644 cpp/s2pctl/s2pctl_parser.cpp create mode 100644 cpp/s2pctl/s2pctl_parser.h create mode 100644 cpp/s2pctl/sp2ctl_core.cpp create mode 100644 cpp/s2pdump/s2pdump.cpp create mode 100644 cpp/s2pdump/s2pdump_core.cpp create mode 100644 cpp/s2pdump/s2pdump_core.h create mode 100644 cpp/s2pdump/s2pdump_executor.cpp create mode 100644 cpp/s2pdump/s2pdump_executor.h create mode 100644 cpp/s2pexec/s2pexec.cpp create mode 100644 cpp/s2pexec/s2pexec_core.cpp create mode 100644 cpp/s2pexec/s2pexec_core.h create mode 100644 cpp/s2pexec/s2pexec_executor.cpp create mode 100644 cpp/s2pexec/s2pexec_executor.h create mode 100644 cpp/shared/network_util.cpp create mode 100644 cpp/shared/network_util.h create mode 100644 cpp/shared/phase_executor.cpp create mode 100644 cpp/shared/phase_executor.h create mode 100644 cpp/shared/protobuf_util.cpp create mode 100644 cpp/shared/protobuf_util.h create mode 100644 cpp/shared/s2p_util.cpp create mode 100644 cpp/shared/s2p_util.h create mode 100644 cpp/shared/s2p_version.h create mode 100644 cpp/shared/scsi.h create mode 100644 cpp/shared/shared_exceptions.h create mode 100644 cpp/test/abstract_controller_test.cpp create mode 100644 cpp/test/bus_test.cpp create mode 100644 cpp/test/cd_dvd_test.cpp create mode 100644 cpp/test/command_context_test.cpp create mode 100644 cpp/test/controller_manager_test.cpp create mode 100644 cpp/test/ctapdriver_test.cpp create mode 100644 cpp/test/daynaport_test.cpp create mode 100644 cpp/test/device_factory_test.cpp create mode 100644 cpp/test/device_test.cpp create mode 100644 cpp/test/disk_test.cpp create mode 100644 cpp/test/host_services_test.cpp create mode 100644 cpp/test/in_process_test.cpp create mode 100644 cpp/test/localizer_test.cpp create mode 100644 cpp/test/mocks.h create mode 100644 cpp/test/mode_page_device_test.cpp create mode 100644 cpp/test/network_util_test.cpp create mode 100644 cpp/test/optical_memory_test.cpp create mode 100644 cpp/test/phase_handler_test.cpp create mode 100644 cpp/test/primary_device_test.cpp create mode 100644 cpp/test/printer_test.cpp create mode 100644 cpp/test/protobuf_util_test.cpp create mode 100644 cpp/test/s2p_executor_test.cpp create mode 100644 cpp/test/s2p_image_test.cpp create mode 100644 cpp/test/s2p_response_test.cpp create mode 100644 cpp/test/s2p_service_test.cpp create mode 100644 cpp/test/s2p_util_test.cpp create mode 100644 cpp/test/s2pctl_commands_test.cpp create mode 100644 cpp/test/s2pctl_display_test.cpp create mode 100644 cpp/test/s2pctl_parser_test.cpp create mode 100644 cpp/test/s2pdump_test.cpp create mode 100644 cpp/test/scsi_command_util_test.cpp create mode 100644 cpp/test/scsi_controller_test.cpp create mode 100644 cpp/test/scsi_hd_test.cpp create mode 100644 cpp/test/shared_exceptions_test.cpp create mode 100644 cpp/test/storage_device_test.cpp create mode 100644 cpp/test/test_setup.cpp create mode 100644 cpp/test/test_shared.cpp create mode 100644 cpp/test/test_shared.h create mode 100644 ide_setup/README create mode 100644 ide_setup/eclipse_code_formatter.xml diff --git a/.github/workflows/arm_cross_compile.yml b/.github/workflows/arm_cross_compile.yml new file mode 100644 index 00000000..bff0e483 --- /dev/null +++ b/.github/workflows/arm_cross_compile.yml @@ -0,0 +1,62 @@ +on: + workflow_call: + inputs: + connect-type: + required: true + type: string + # Debug flag indicates whether to build a debug build (no optimization, debugger symbols) + debug-flag: + required: false + type: boolean + +jobs: + build_arm: + runs-on: ubuntu-22.04 + defaults: + run: + working-directory: cpp + env: + APT_ARM_TOOLCHAIN: "gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf binutils-arm-linux-gnueabihf libspdlog-dev" + APT_LIBRARIES: "libspdlog-dev:armhf libpcap-dev:armhf protobuf-compiler libprotobuf-dev:armhf" + steps: + - uses: actions/checkout@v3 + + - name: Add armhf as architecture + run: sudo dpkg --add-architecture armhf + + - name: Reconfigure apt for arch amd64 + run: sudo sed -i "s/deb /deb [arch=amd64] /g" /etc/apt/sources.list + + - name: Add armhf repos (jammy) + run: sudo bash -c "echo \"deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports/ jammy main multiverse restricted universe\" >> /etc/apt/sources.list" + + - name: Add armhf repos (jammy-updates) + run: sudo bash -c "echo \"deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports/ jammy-updates main multiverse restricted universe\" >> /etc/apt/sources.list" + + - name: Update apt + run: sudo apt update + + - name: Install apt packages + run: sudo apt-get --yes install ${{ env.APT_ARM_TOOLCHAIN }} ${{ env.APT_LIBRARIES }} + + - name: Build debug strings + if: ${{ inputs.debug-flag }} + run: | + echo "debug_flag_compile=DEBUG\=1" >> $GITHUB_ENV + echo "debug_flag_filename=debug-" >> $GITHUB_ENV + + - name: Compile + run: make all -j 6 CONNECT_TYPE=${{ inputs.connect-type }} ${{ env.debug_flag_compile }} CROSS_COMPILE=arm-linux-gnueabihf- + + # We need to tar the binary outputs to retain the executable + # file permission. Currently, actions/upload-artifact only + # supports .ZIP files. + # This is workaround for https://github.com/actions/upload-artifact/issues/38 + - name: Tar binary output + run: tar -czvf ${{ env.debug_flag_filename }}piscsi-${{ inputs.connect-type }}.tar.gz ./bin + + - name: Upload binaries + uses: actions/upload-artifact@v3 + with: + name: ${{ env.debug_flag_filename }}arm-binaries-${{ inputs.connect-type }}.tar.gz + path: cpp/${{ env.debug_flag_filename }}piscsi-${{ inputs.connect-type }}.tar.gz diff --git a/.github/workflows/build_code.yml b/.github/workflows/build_code.yml new file mode 100644 index 00000000..45842b9d --- /dev/null +++ b/.github/workflows/build_code.yml @@ -0,0 +1,15 @@ +name: Compile on macos + +on: + workflow_dispatch: + push: + paths: + - 'cpp/**' + - '.github/workflows/macos_compile.yml' + +jobs: + debug-fullspec: + uses: uweseimet/scsi2pi/.github/workflows/macos_compile.yml@main + with: + connect-type: "FULLSPEC" + debug-flag: true diff --git a/.github/workflows/cpp.yml b/.github/workflows/cpp.yml new file mode 100644 index 00000000..87cdca76 --- /dev/null +++ b/.github/workflows/cpp.yml @@ -0,0 +1,107 @@ +name: Unit tests; SonarCloud analysis + +on: + workflow_dispatch: + push: + paths: + - 'cpp/**' + - '.github/workflows/cpp.yml' + pull_request: + paths: + - 'cpp/**' + - '.github/workflows/cpp.yml' + branches: + - 'develop' + - 'main' + +env: + APT_PACKAGES: libspdlog-dev libpcap-dev protobuf-compiler libgtest-dev libgmock-dev + +jobs: + unit_tests: + runs-on: ubuntu-22.04 + defaults: + run: + working-directory: cpp + steps: + - uses: actions/checkout@v3 + + - name: Install dependencies + run: sudo apt-get install ${{ env.APT_PACKAGES }} + + - name: Build unit tests + run: make -j $(nproc) test + + - name: Run unit tests + run: (set -o pipefail && bin/piscsi_test | tee piscsi_test_log.txt) + + sonarcloud: + runs-on: ubuntu-22.04 + if: github.repository == 'uweseimet/scsi2pi' + env: + SOURCES: cpp + BUILD_WRAPPER_OUT_DIR: "$HOME/.build_wrapper_out" # Directory where build-wrapper output will be placed + SONAR_SCANNER_VERSION: 5.0.1.3006 + SONAR_SERVER_URL: "https://sonarcloud.io" + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_PROJECT_KEY: "scsi2pi" + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + + - name: Install dependencies + run: sudo apt-get install ${{ env.APT_PACKAGES }} + + - name: Set up JDK 17 + uses: actions/setup-java@v1 + with: + java-version: 17 + + - uses: actions/cache@v3 + name: Cache SonarCloud setup + id: sonar-install-cache + with: + path: ~/.sonar + key: sonar-with-build-wrapper-${{ env.SONAR_SCANNER_VERSION }} + + - name: Set up SonarCloud scanner + if: steps.sonar-install-cache.outputs.cache-hit != 'true' + env: + SONAR_SCANNER_DOWNLOAD_URL: https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${{ env.SONAR_SCANNER_VERSION }}-linux.zip + run: | + mkdir -p $HOME/.sonar + curl -sSLo /tmp/sonar-scanner.zip ${{ env.SONAR_SCANNER_DOWNLOAD_URL }} + unzip -o /tmp/sonar-scanner.zip -d $HOME/.sonar/ + + - name: Set up SonarCloud build wrapper + if: steps.sonar-install-cache.outputs.cache-hit != 'true' + env: + BUILD_WRAPPER_DOWNLOAD_URL: ${{ env.SONAR_SERVER_URL }}/static/cpp/build-wrapper-linux-x86.zip + run: | + curl -sSLo /tmp/build-wrapper-linux-x86.zip ${{ env.BUILD_WRAPPER_DOWNLOAD_URL }} + unzip -o /tmp/build-wrapper-linux-x86.zip -d $HOME/.sonar/ + + - name: Generate coverage + run: >- + (mkdir -p ${{ env.BUILD_WRAPPER_OUT_DIR }} || true) && + $HOME/.sonar/build-wrapper-linux-x86/build-wrapper-linux-x86-64 + --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} + make -j $(nproc) -C $SOURCES coverage + + - name: Run gcov + working-directory: cpp + run: gcov --preserve-paths $(find -name '*.gcno') + + - name: Run sonar-scanner + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: >- + $HOME/.sonar/sonar-scanner-${{ env.SONAR_SCANNER_VERSION }}-linux/bin/sonar-scanner + --define sonar.host.url="${{ env.SONAR_SERVER_URL }}" + --define sonar.projectKey=${{ env.SONAR_PROJECT_KEY }} + --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}" + --define sonar.cfamily.gcov.reportsPath=. + --define sonar.coverage.exclusions="cpp/**/test/**" + --define sonar.cpd.exclusions="cpp/**/test/**" + --define sonar.inclusions="cpp/** diff --git a/.github/workflows/macos_compile.yml b/.github/workflows/macos_compile.yml new file mode 100644 index 00000000..08fb0eba --- /dev/null +++ b/.github/workflows/macos_compile.yml @@ -0,0 +1,34 @@ +on: + workflow_call: + inputs: + connect-type: + required: true + type: string + # Debug flag indicates whether to build a debug build (no optimization, debugger symbols) + debug-flag: + required: false + type: boolean + +jobs: + build_macos: + runs-on: macos-13 + defaults: + run: + working-directory: cpp + steps: + - uses: actions/checkout@v3 + + - name: Install Homebrew + run: bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + + - name: Install packages + run: brew install protobuf spdlog + + - name: Build debug strings + if: ${{ inputs.debug-flag }} + run: | + echo "debug_flag_compile=DEBUG\=1" >> $GITHUB_ENV + echo "debug_flag_filename=debug-" >> $GITHUB_ENV + + - name: Compile + run: make all -j 6 CONNECT_TYPE=${{ inputs.connect-type }} ${{ env.debug_flag_compile }} diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..4213085f --- /dev/null +++ b/LICENSE @@ -0,0 +1,33 @@ +BSD 3-Clause License + +Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +Copyright (C) 2014-2020 GIMONS +Copyright (C) 2020-2021 akuker +Copyright (C) PiSCSI project contributors (github.com/PiSCSI/piscsi) +Copyright (C) 2021-2023 Uwe Seimet +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..c507bc0a --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# What is SCSI2Pi? + +The PiSCSI/RaSCSI board extends the Raspberry Pi with a SCSI interface. SCSI2Pi (or simply just S2P) is an alternative to the PiSCSI software. SCSI2Pi focuses on improving the backend and is compatible with the PiSCSI web UI and the PiSCSI Control app.
+You can switch from PiSCSI to SCSI2Pi (or back, if needed) in seconds, by installing/de-installing a Debian package. This package will contain the SCSI2Pi-Binaries, i.e. there is no time-consuming compilation required. At least that's the plan.
+SCSI2Pi emulates several SCSI or SASI devices like hard disks, CD-ROM drives, printers or network adapters at the same time. This way you can easily add devices to computers like 68k Macs, Atari ST/TT/Falcon, Amigas, old PCs, Unix workstations or samplers. Compared to PiSCSI the SCSI2Pi backend offers numerous extensions, new device emulations, performance improvements and bug fixes. These add to the extensive changes I contributed to the PiSCSI project in the past. Almost the complete original codebase I have re-implemented in modern C++.
+SCSI2Pi was chosen as the name for this project because there are not that many choices anymore when looking for a name that contains both "SCSI" and "Pi" ;-).
+The SCSI2Pi project has not yet been fully set up and it will still take a bit of time until there is a first release. Note that the current sources on GitHub are only temporary, and the repository will be re-initialized with fresh sources when SCSI2Pi is ready to start. + +# Who am I? + +In the past I was the main C++ code contributor for PiSCSI. I re-engineered most of the legacy C++ code and updated it to C++-20, which is the latest C++ standard you can currently use on the Pi. I also revised the complete device emulation architecture. All in all this resulted in more modular code and drastically improved SonarQube code metrics. Besides adding numerous new features and improving the compatibility with many platforms, I also fixed numerous bugs in the legacy codebase and added an extensive set of unit tests. Further I am the author of the PiSCSI Control app for Android, which is the remote control for your RaSCSI/PiSCSI boards. + +# How is SCSI2Pi related to PiSCSI and the former RaSCSI? + +Over time, within the PiSCSI project the interest in replacing the old, often buggy or unnecessary code became rather low. In addition, developers for the required pull request reviews were missing, and many of the features that have been promised for a long time are not addressed. This is why I decided to further improve the software in a separate project. The major part of the current (23.11.01) PiSCSI codebase has been contributed by me anyway. +There was also no interest in further exploiting the initiator mode feature of the FULLSPEC board. This mode, together with new command line tools, offers solutions for use cases that have never been addressed before, neither with PiSCSI nor its predecessor RaSCSI. +From my perspective this situation was not satisfying, and this is why I decided to set up SCSI2Pi as an alternative project that is compatible with the RaSCSI/PiSCSI board. + +# SCSI2Pi goals + +The intention of SCSI2Pi is not to completely replace the PiSCSI software, where great work is still being done on the web interface, for instance, and on supporting users in social media. +The goal of SCSI2Pi is to improve the actual SCSI emulation, i.e. the C++ core, and to address compatibility issues, mainly (but not only) with usually rather exotic platforms. SCSI2Pi focuses on vintage computers like Macs, Ataris, Amigas, workstations and on samplers. There is no support for the X68000, in particular the host bridge, though. In PiSCSI the related code has always been in a bad shape, nobody has been willing to test it. It might not even be working. Most of the other features of PiSCSI are supported by SCSI2Pi - many of these I implemented anyway ;-). There are more new features to come. + +# How to switch between PiSCSI and SCSI2Pi + +As already mentioned, SCSI2Pi is not meant to completely replace PiSCSI. Instead, I intend to provide standard Debian packages with binaries that replace the PiSCSI backend binaries. Just by installing such a package your PiSCSI installation will profit from a more modern and well maintained SCSI emulation codebase.
+If need should be, you can switch back any time by just running PiSCSI's easysetup script. SCSI2Pi does not modify anything except binaries and manpages, which can easily be reverted.
+Naturally, in case you have to switch back to PiSCSI I would appreciate your feedback on any issues caused by SCSI2Pi which you do not observe with PiSCSI 23.11.01, which is the last release I have contributed to. Just create a GitHub ticket with details on what is working with PiSCSI but not with SCSI2Pi. But please note: The SCSI2Pi project only accepts tickets that target SCSI2Pi, but none related to PiSCSI.
+Even though I cannot give any guarantees I will try to stay compatible with PiSCSI on the one hand while on the other hand offering new backend features. The same applies to the PiSCSI Control Android app, which is going to be renamed to SCSI Control. I will try to keep it compatible also with PiSCSI. Currently I do not see an issue with that. +Being compatible and not a full replacement also means being compatible with most of the documentation on the PiSCSI Wiki. More general information on SCSI2Pi is available on the SCSI2Pi website. The GitHub site focuses on development work. + +# Contributing to SCSI2Pi + +If you are interested in the Pi and/or SCSI, in modern C++ and platform-independent programming, you are very welcome as a developer or a code reviewer.
+Did I just say "platform independent programming", even though SCSI2Pi is about the Pi? I did indeed, because I have ensured that the PiSCSI code also compiles and partially runs on regular Linux PCs, on BSD and even on macos. This is important for developers and for testing, because the faster your development machine, the better. In general, a C provides a much better development environment than a Pi. I am using Eclipse CDT on a Linux PC as my primary development platform, by the way.
+Also see the developer notes on the SCSI2Pi website. diff --git a/cpp/Makefile b/cpp/Makefile new file mode 100644 index 00000000..135922a1 --- /dev/null +++ b/cpp/Makefile @@ -0,0 +1,377 @@ +.DEFAULT_GOAL: all + +## Optional build flags: +## CROSS_COMPILE= Specifies which compiler toolchain to use. +## To cross compile set this accordingly, e.g. to: +## arm-linux-gnueabihf- +CROSS_COMPILE = + +CXX = $(CROSS_COMPILE)g++ +AR = $(CROSS_COMPILE)ar + +## DEBUG=1 A Debug build includes the debugger symbols and +## disables compiler optimization. This is usually +## only by developers. +DEBUG ?= 0 +ifeq ($(DEBUG), 1) + # Debug compiler flags + CXXFLAGS += -O0 -g -DDEBUG +else + # Release compiler flags + CXXFLAGS += -O3 -Wall -DNDEBUG +endif + +CXXFLAGS += -std=c++20 -iquote . -D_FILE_OFFSET_BITS=64 -DFMT_HEADER_ONLY -DSPDLOG_FMT_EXTERNAL -MD -MP -Wall -Wextra + +ifeq ("$(shell uname -s)", "Linux") + # -Wno-psabi might not work on non-Linux platforms + CXXFLAGS += -Wno-psabi +endif + +ifeq ("$(shell uname -s)", "FreeBSD") + CXXFLAGS += -fexperimental-library -I/usr/local/include -Wno-unused-parameter +endif + +ifeq ("$(shell uname -s)", "NetBSD") + CXXFLAGS += -fexperimental-library -I/usr/pkg/include -Wno-unused-parameter -Wno-macro-redefined +endif + +## EXTRA_FLAGS Can be used to pass special purpose flag +CXXFLAGS += $(EXTRA_FLAGS) + +## Installation path +## The path to install binaries and manpages in, default is /usr/local +TARGET=/usr/local + +## CONNECT_TYPE=FULLSPEC +## Specifies the board type that you are using. Typically this is +## STANDARD or FULLSPEC. The default is FULLSPEC. +CONNECT_TYPE ?= FULLSPEC + +ifdef CONNECT_TYPE + CXXFLAGS += -DCONNECT_TYPE_$(CONNECT_TYPE) +endif + +## DEVICES=LIST +## Lists of devices to build, default is all. Supported devices: +## SCHD SCSI Removable and non-removable Hard Drive +## SCCD SCSI CD-Rom Drive +## SCMO SCSI Optical Memory +## SCDP DaynaPort Network Adapter +## SCLP SCSI Printer +## SCHS Host Services +## SAHD SASI Hard Drive +ifeq ($(or $(BUILD_SCHD),$(BUILD_SCMO),$(BUILD_SCCD),$(BUILD_SCDP),$(BUILD_SCLP),$(BUILD_SCHS),$(BUILD_SAHD)),) + BUILD_SCHD = 1 + BUILD_SCMO = 1 + BUILD_SCCD = 1 + BUILD_SCDP = 1 + BUILD_SCLP = 1 + BUILD_SCHS = 1 + BUILD_SAHD = 1 +endif +ifdef BUILD_SCHD + CXXFLAGS += -DBUILD_MODE_PAGE_DEVICE -DBUILD_DISK -DBUILD_SCHD +endif +ifdef BUILD_SCMO + CXXFLAGS += -DBUILD_MODE_PAGE_DEVICE -DBUILD_DISK -DBUILD_SCMO +endif +ifdef BUILD_SCCD + CXXFLAGS += -DBUILD_MODE_PAGE_DEVICE -DBUILD_DISK -DBUILD_SCCD +endif +ifdef BUILD_SCDP + CXXFLAGS += -DBUILD_SCDP +endif +ifdef BUILD_SCLP + CXXFLAGS += -DBUILD_SCLP +endif +ifdef BUILD_SCHS + CXXFLAGS += -DBUILD_MODE_PAGE_DEVICE -DBUILD_SCHS +endif +ifdef BUILD_SAHD + CXXFLAGS += -DBUILD_DISK -DBUILD_SAHD +endif + +S2P = s2p +S2PCTL = s2pctl +S2PDUMP = s2pdump +S2PEXEC = s2pexec +S2P_TEST = s2p_test +IN_PROCESS_TEST = in_process_test + +INSTALL_BIN = $(TARGET)/bin +MAN_PAGE_DIR = $(TARGET)/man/man1 +DOC_DIR = doc +COVERAGE_DIR = coverage +COVERAGE_FILE = sp2.dat + +OBJDIR := obj +LIBDIR := lib +BINDIR := bin + +BIN_ALL = \ + $(BINDIR)/$(S2P) \ + $(BINDIR)/$(S2PCTL) + +# s2pdump and s2pexec require initiator support +ifeq ($(CONNECT_TYPE), FULLSPEC) + BIN_ALL += $(BINDIR)/$(S2PDUMP) + BIN_ALL += $(BINDIR)/$(S2PEXEC) +endif + +SRC_PROTOC = command_interface.proto + +SRC_GENERATED = $(GENERATED_DIR)/command_interface.pb.cpp + +SRC_SHARED = $(shell ls -1 shared/*.cpp | grep -v protobuf_util.cpp) +SRC_SHARED_PROTOBUF = $(SRC_SHARED) +SRC_SHARED_PROTOBUF += shared/protobuf_util.cpp +SRC_BASE = $(shell ls -1 base/*.cpp) +SRC_BUSES = $(shell ls -1 buses/*.cpp) +SRC_CONTROLLERS = $(shell ls -1 controllers/*.cpp) + +SRC_DISK = \ + devices/disk.cpp \ + devices/disk_cache.cpp \ + devices/disk_track.cpp \ + devices/storage_device.cpp \ + devices/mode_page_device.cpp + +ifdef BUILD_SCHD +SRC_SCHD = \ + devices/scsi_hd.cpp +SRC_SCHD += $(SRC_DISK) +endif + +ifdef BUILD_SCMO +SRC_SCMO = \ + devices/optical_memory.cpp +SRC_SCMO += $(SRC_DISK) +endif + +ifdef BUILD_SCCD +SRC_SCCD = \ + devices/cd_dvd.cpp \ + devices/cd_track.cpp +SRC_SCCD += $(SRC_DISK) +endif + +ifdef BUILD_SCDP +SRC_SCDP = \ + devices/daynaport.cpp \ + devices/ctapdriver.cpp +endif + +ifdef BUILD_SCLP +SRC_SCLP = \ + devices/printer.cpp +endif + +ifdef BUILD_SCHS +SRC_SCHS = \ + devices/host_services.cpp \ + devices/mode_page_device.cpp +endif + +ifdef BUILD_SAHD +SRC_SAHD = \ + devices/sasi_hd.cpp +SRC_SAHD += $(SRC_DISK) +endif + +SRC_S2P_CORE = $(shell ls -1 s2p/*.cpp | grep -v s2p.cpp) +SRC_S2P = s2p/s2p.cpp + +SRC_S2PCTL_CORE = $(shell ls -1 s2pctl/*.cpp | grep -v s2pctl.cpp) +SRC_S2PCTL = s2pctl/s2pctl.cpp + +SRC_S2PDUMP = s2pdump/s2pdump.cpp +SRC_S2PDUMP += $(shell ls -1 s2pdump/*.cpp | grep -v s2pdump.cpp) + +SRC_S2PEXEC := $(shell ls -1 s2pexec/*.cpp) + +SRC_S2P_TEST = $(shell ls -1 test/*.cpp | grep -v in_process_) +SRC_S2P_TEST += $(shell ls -1 s2pdump/*.cpp | grep -v s2pdump.cpp) + +SRC_IN_PROCESS_TEST = test/in_process_test.cpp +SRC_IN_PROCESS_TEST += $(shell ls -1 s2pdump/*.cpp | grep -v s2pdump.cpp) + +vpath %.h ./shared ./base ./controllers ./buses ./s2p ./s2pctl ./s2pdump ./s2pexec +vpath %.cpp ./shared ./base ./controllers ./devices ./buses ./s2p ./s2pctl ./s2pdump ./s2pexec ./test +vpath %.o ./$(OBJDIR) +vpath ./$(BINDIR) + +LIB_SHARED := $(LIBDIR)/libshared.a +LIB_SHARED_PROTOBUF := $(LIBDIR)/libsharedprotobuf.a +LIB_BUS := $(LIBDIR)/libbus.a +LIB_CONTROLLER := $(LIBDIR)/libcontroller.a +LIB_DEVICE := $(LIBDIR)/libdevice.a + +OBJ_SCHD := $(addprefix $(OBJDIR)/,$(notdir $(SRC_SCHD:%.cpp=%.o))) +OBJ_SCMO := $(addprefix $(OBJDIR)/,$(notdir $(SRC_SCMO:%.cpp=%.o))) +OBJ_SCCD := $(addprefix $(OBJDIR)/,$(notdir $(SRC_SCCD:%.cpp=%.o))) +OBJ_SCDP := $(addprefix $(OBJDIR)/,$(notdir $(SRC_SCDP:%.cpp=%.o))) +OBJ_SCLP := $(addprefix $(OBJDIR)/,$(notdir $(SRC_SCLP:%.cpp=%.o))) +OBJ_SCHS := $(addprefix $(OBJDIR)/,$(notdir $(SRC_SCHS:%.cpp=%.o))) +OBJ_SAHD := $(addprefix $(OBJDIR)/,$(notdir $(SRC_SAHD:%.cpp=%.o))) +OBJ_SHARED := $(addprefix $(OBJDIR)/,$(notdir $(SRC_SHARED:%.cpp=%.o))) +OBJ_SHARED_PROTOBUF = $(addprefix $(OBJDIR)/,$(notdir $(SRC_SHARED_PROTOBUF:%.cpp=%.o))) +OBJ_BASE := $(addprefix $(OBJDIR)/,$(notdir $(SRC_BASE:%.cpp=%.o))) +OBJ_BUSES := $(addprefix $(OBJDIR)/,$(notdir $(SRC_BUSES:%.cpp=%.o))) +OBJ_CONTROLLERS := $(addprefix $(OBJDIR)/,$(notdir $(SRC_CONTROLLERS:%.cpp=%.o))) +OBJ_DEVICES := $(shell echo $(OBJ_SCHD) $(OBJ_SCMO) $(OBJ_SCCD) $(OBJ_SCDP) $(OBJ_SCLP) $(OBJ_SCHS) $(OBJ_SAHD) | xargs -n1 | sort -u | xargs) +OBJ_S2P_CORE := $(addprefix $(OBJDIR)/,$(notdir $(SRC_S2P_CORE:%.cpp=%.o))) +OBJ_S2P := $(addprefix $(OBJDIR)/,$(notdir $(SRC_S2P:%.cpp=%.o))) +OBJ_S2PCTL_CORE := $(addprefix $(OBJDIR)/,$(notdir $(SRC_S2PCTL_CORE:%.cpp=%.o))) +OBJ_S2PCTL := $(addprefix $(OBJDIR)/,$(notdir $(SRC_S2PCTL:%.cpp=%.o))) +OBJ_S2PDUMP := $(addprefix $(OBJDIR)/,$(notdir $(SRC_S2PDUMP:%.cpp=%.o))) +OBJ_S2PEXEC := $(addprefix $(OBJDIR)/,$(notdir $(SRC_S2PEXEC:%.cpp=%.o))) +OBJ_GENERATED := $(addprefix $(OBJDIR)/,$(notdir $(SRC_GENERATED:%.cpp=%.o))) +OBJ_S2P_TEST := $(addprefix $(OBJDIR)/,$(notdir $(SRC_S2P_TEST:%.cpp=%.o))) +OBJ_IN_PROCESS_TEST := $(addprefix $(OBJDIR)/,$(notdir $(SRC_IN_PROCESS_TEST:%.cpp=%.o))) + + +BINARIES = $(INSTALL_BIN)/$(S2PCTL) \ + $(INSTALL_BIN)/$(S2P) +ifeq ($(CONNECT_TYPE), FULLSPEC) + BINARIES += $(INSTALL_BIN)/$(S2PDUMP) + BINARIES += $(INSTALL_BIN)/$(S2PEXEC) +endif + +MAN_PAGES = $(MAN_PAGE_DIR)/s2p.1 \ + $(MAN_PAGE_DIR)/s2pctl.1 +ifeq ($(CONNECT_TYPE), FULLSPEC) + MAN_PAGES += $(MAN_PAGE_DIR)/s2pdump.1 + MAN_PAGES += $(MAN_PAGE_DIR)/s2pexec.1 +endif + +GENERATED_DIR := generated + +# The following will include all of the auto-generated dependency files (*.d) +# if they exist. This will trigger a rebuild of a source file if a header changes +ALL_DEPS := $(patsubst %.o,%.d,$(OBJ_S2P_CORE) $(OBJ_S2PCTL_CORE) $(OBJ_S2P) $(OBJ_S2PCTL) $(OBJ_S2PDUMP) \ + $(OBJ_S2PEXEC) $(OBJ_SHARED) $(OBJ_SHARED_PROTOBUF) $(OBJ_BASE) $(OBJ_DEVICES) $(OBJ_S2P_TEST) $(OBJ_IN_PROCESS_TEST)) +-include $(ALL_DEPS) + +$(OBJDIR) $(LIBDIR) $(BINDIR): + mkdir -p $@ + +$(OBJDIR)/%.o: %.cpp | $(OBJDIR) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +$(LIB_SHARED): $(OBJ_SHARED) | $(LIBDIR) + $(AR) rcs $@ $^ + +$(LIB_SHARED_PROTOBUF): $(OBJ_SHARED_PROTOBUF) $(OBJ_GENERATED) | $(LIBDIR) + $(AR) rcs $@ $^ + +$(LIB_BUS): $(OBJ_BUSES) | $(LIBDIR) + $(AR) rcs $@ $^ + +$(LIB_CONTROLLER): $(OBJ_CONTROLLERS) | $(LIBDIR) + $(AR) rcs $@ $^ + +$(LIB_DEVICE): $(OBJ_DEVICES) $(OBJ_BASE) | $(LIBDIR) + $(AR) rcs $@ $^ + +$(SRC_GENERATED): $(SRC_PROTOC) + mkdir -p $(GENERATED_DIR) + protoc --cpp_out=$(GENERATED_DIR) $(SRC_PROTOC) + mv $(GENERATED_DIR)/command_interface.pb.cc $@ + +$(OBJ_GENERATED): $(SRC_GENERATED) | $(OBJDIR) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +## Build targets: +## all Rebuild all of the executable files and re-generate +## the text versions of the manpages +## docs Re-generate the text versions of the man pages +## test Build and run unit tests +## in_process_test Build the in-process test +## coverage Build and run unit tests and create coverage files +## lcov Build and run unit tests and create coverage HTML files +## Note that you have to run 'make clean' before switching +## between coverage and non-coverage builds. +.DEFAULT_GOAL := all +.PHONY: all docs test coverage lcov + +all: $(SRC_GENERATED) $(BIN_ALL) docs + +test: $(SRC_GENERATED) $(BINDIR)/$(S2P_TEST) + $(BINDIR)/$(S2P_TEST) + +in_process_test: $(SRC_GENERATED) $(BINDIR)/$(IN_PROCESS_TEST) + +coverage: CXXFLAGS += --coverage +coverage: test + +lcov: CXXFLAGS += --coverage +lcov: test + lcov -q -c -d . --include '*/cpp/*' -o $(COVERAGE_FILE) --exclude '*/test/*' --exclude '*/interfaces/*' --exclude '*/command_interface.pb*' + genhtml -q -o $(COVERAGE_DIR) --legend $(COVERAGE_FILE) + +docs: $(DOC_DIR)/s2p_man_page.txt $(DOC_DIR)/s2pctl_man_page.txt $(DOC_DIR)/s2pdump_man_page.txt $(DOC_DIR)/s2pexec_man_page.txt + +$(SRC_S2P_CORE) $(SRC_S2PCTL_CORE) $(SRC_S2PEXEC): $(OBJ_GENERATED) + +$(BINDIR)/$(S2P): $(LIB_SHARED_PROTOBUF) $(LIB_BUS) $(LIB_CONTROLLER) $(LIB_DEVICE) $(OBJ_S2P_CORE) $(OBJ_S2P)| $(BINDIR) + $(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $(OBJ_S2P_CORE) $(OBJ_S2P) $(LIB_SHARED_PROTOBUF) $(LIB_BUS) $(LIB_CONTROLLER) $(LIB_DEVICE) -lpthread -lpcap -lprotobuf + +$(BINDIR)/$(S2PCTL): $(LIB_SHARED_PROTOBUF) $(OBJ_S2PCTL_CORE) $(OBJ_S2PCTL) | $(BINDIR) + $(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $(OBJ_S2PCTL_CORE) $(OBJ_S2PCTL) $(LIB_SHARED_PROTOBUF) -lprotobuf + +$(BINDIR)/$(S2PDUMP): $(OBJ_S2PDUMP) $(LIB_SHARED) $(LIB_BUS)| $(BINDIR) + $(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $(OBJ_S2PDUMP) $(LIB_SHARED) $(LIB_BUS) + +$(BINDIR)/$(S2PEXEC): $(OBJ_S2PEXEC) $(LIB_SHARED_PROTOBUF) $(LIB_BUS) | $(BINDIR) + $(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $(OBJ_S2PEXEC) $(LIB_SHARED_PROTOBUF) $(LIB_BUS) -lprotobuf + +$(BINDIR)/$(S2P_TEST): $(LIB_SHARED_PROTOBUF) $(LIB_BUS) $(LIB_CONTROLLER) $(LIB_DEVICE) $(OBJ_S2P_CORE) $(OBJ_S2PCTL_CORE) $(OBJ_S2P_TEST) $(OBJ_S2PCTL_TEST)| $(BINDIR) + $(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $(OBJ_S2P_CORE) $(OBJ_S2PCTL_CORE) $(OBJ_S2P_TEST) $(LIB_SHARED_PROTOBUF) $(LIB_BUS) $(LIB_CONTROLLER) $(LIB_DEVICE) -lpthread -lpcap -lprotobuf -lgmock -lgtest + +$(BINDIR)/$(IN_PROCESS_TEST): $(LIB_SHARED_PROTOBUF) $(LIB_BUS) $(LIB_CONTROLLER) $(LIB_DEVICE) $(OBJ_S2P_CORE) $(OBJ_IN_PROCESS_TEST) | $(BINDIR) + $(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $(OBJ_S2P_CORE) $(OBJ_IN_PROCESS_TEST) $(LIB_SHARED_PROTOBUF) $(LIB_BUS) $(LIB_CONTROLLER) $(LIB_DEVICE) -lpthread -lpcap -lprotobuf + +# Phony rules for building individual utilities +.PHONY: $(S2P) $(S2PCTL) $(S2PDUMP) $(S2PEXEC) $(S2P_TEST) $(IN_PROCESS_TEST) +$(S2P): $(BINDIR)/$(S2P) +$(S2PCTL): $(BINDIR)/$(S2PCTL) +$(S2PDUMP): $(BINDIR)/$(S2PDUMP) +$(S2PEXEC): $(BINDIR)/$(S2PEXEC) +$(S2P_TEST): $(BINDIR)/$(S2P_TEST) +$(IN_PROCESS_TEST): $(BINDIR)/$(IN_PROCESS_TEST) + +## clean Remove all of the object files, intermediate +## compiler files and executable files +.PHONY: clean +clean: + rm -rf $(OBJDIR) $(LIBDIR) $(BINDIR) $(GENERATED_DIR) $(COVERAGE_DIR) $(COVERAGE_FILE) + +## install Copies all of the man pages, usually to /usr/local/man/man1 +## Copies the binaries, usually to /usr/local/bin +.PHONY: install +install: \ + $(MAN_PAGES) \ + $(BINARIES) + +$(INSTALL_BIN)%: $(BINDIR)/% + mkdir -p $(INSTALL_BIN) + cp $< $@ + $(CROSS_COMPILE)strip $@ + +$(MAN_PAGE_DIR)/%.1: $(DOC_DIR)/%.1 | $(MAN_PAGE_DIR)/ + cp $< $@ + +$(DOC_DIR)/%_man_page.txt: $(DOC_DIR)/%.1 + man -l $< | col -bx >> $@ + +$(MAN_PAGE_DIR)/: + mkdir -p $@ + +## help Lists information about how to use the Makefile +# The help rule is based upon the approach from: +# https://swcarpentry.github.io/make-novice/08-self-doc/index.html +.PHONY: help +help: Makefile + @sed -n 's/^##//p' $< + diff --git a/cpp/base/device.cpp b/cpp/base/device.cpp new file mode 100644 index 00000000..637fa7d2 --- /dev/null +++ b/cpp/base/device.cpp @@ -0,0 +1,138 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/s2p_version.h" +#include "device.h" +#include +#include +#include + +using namespace std; + +Device::Device(PbDeviceType type, int lun) : type(type), lun(lun) +{ + revision = fmt::format("{0:02}{1:02}", s2p_major_version, s2p_minor_version); +} + +void Device::Reset() +{ + locked = false; + attn = false; + reset = false; +} + +void Device::SetProtected(bool b) +{ + if (protectable && !read_only) { + write_protected = b; + } +} + +void Device::SetVendor(const string& v) +{ + if (v.empty() || v.length() > 8) { + throw invalid_argument("Vendor '" + v + "' must have between 1 and 8 characters"); + } + + vendor = v; +} + +void Device::SetProduct(const string& p, bool force) +{ + if (p.empty() || p.length() > 16) { + throw invalid_argument("Product '" + p + "' must have between 1 and 16 characters"); + } + + // Changing vital product data is not SCSI compliant + if (!product.empty() && !force) { + return; + } + + product = p; +} + +void Device::SetRevision(const string& r) +{ + if (r.empty() || r.length() > 4) { + throw invalid_argument("Revision '" + r + "' must have between 1 and 4 characters"); + } + + revision = r; +} + +string Device::GetPaddedName() const +{ + return fmt::format("{0:8}{1:16}{2:4}", vendor, product, revision); +} + +string Device::GetParam(const string& key) const +{ + const auto& it = params.find(key); + return it == params.end() ? "" : it->second; +} + +void Device::SetParams(const param_map& set_params) +{ + params = GetDefaultParams(); + + // Devices with image file support implicitly support the "file" parameter + if (SupportsFile()) { + params["file"] = ""; + } + + for (const auto& [key, value] : set_params) { + // It is assumed that there are default parameters for all supported parameters + if (params.contains(key)) { + params[key] = value; + } + else { + spdlog::warn("Ignored unknown parameter '" + key + "'"); + } + } +} + +bool Device::Start() +{ + if (!ready) { + return false; + } + + stopped = false; + + return true; +} + +void Device::Stop() +{ + ready = false; + attn = false; + stopped = true; + + status_code = 0; +} + +bool Device::Eject(bool force) +{ + if (!ready || !removable) { + return false; + } + + // Must be unlocked if there is no force flag + if (!force && locked) { + return false; + } + + ready = false; + attn = false; + removed = true; + write_protected = false; + locked = false; + stopped = true; + + return true; +} diff --git a/cpp/base/device.h b/cpp/base/device.h new file mode 100644 index 00000000..fc719407 --- /dev/null +++ b/cpp/base/device.h @@ -0,0 +1,145 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "generated/command_interface.pb.h" +#include "shared/s2p_util.h" +#include +#include + +using namespace std; +using namespace command_interface; + +// A combination of device ID and LUN +using id_set = pair; + +// The map used for storing/passing device parameters +using param_map = unordered_map>; + +class Device //NOSONAR The number of fields and methods is justified, the complexity is low +{ + const string DEFAULT_VENDOR = "SCSI2Pi"; + + PbDeviceType type; + + bool ready = false; + bool reset = false; + bool attn = false; + + // Device is protectable/write-protected + bool protectable = false; + bool write_protected = false; + // Device is permanently read-only + bool read_only = false; + + // Device can be stopped (parked)/is stopped (parked) + bool stoppable = false; + bool stopped = false; + + // Device is removable/removed + bool removable = false; + bool removed = false; + + // Device is lockable/locked + bool lockable = false; + bool locked = false; + + // A device can be created with parameters + bool supports_params = false; + + // A device can support an image file + bool supports_file = false; + + // Immutable LUN + int lun; + + // Device identifier (for INQUIRY) + string vendor = DEFAULT_VENDOR; + string product; + string revision; + + // The parameters the device was created with + param_map params; + + // Sense Key and ASC + // MSB Reserved (0x00) + // Sense Key + // Additional Sense Code (ASC) + int status_code = 0; + +protected: + + Device(PbDeviceType, int); + + void SetReady(bool b) { ready = b; } + bool IsReset() const { return reset; } + void SetReset(bool b) { reset = b; } + bool IsAttn() const { return attn; } + void SetAttn(bool b) { attn = b; } + + void SetRemovable(bool b) { removable = b; } + void SetStoppable(bool b) { stoppable = b; } + void SetStopped(bool b) { stopped = b; } + void SetLockable(bool b) { lockable = b; } + void SetLocked(bool b) { locked = b; } + + int GetStatusCode() const { return status_code; } + + string GetParam(const string&) const; + void SetParams(const param_map&); + +public: + + virtual ~Device() = default; + + PbDeviceType GetType() const { return type; } + string GetTypeString() const { return PbDeviceType_Name(type); } + string GetIdentifier() const { return GetTypeString() + " " + to_string(GetId()) + ":" + to_string(lun); } + + bool IsReady() const { return ready; } + virtual void Reset(); + + bool IsProtectable() const { return protectable; } + void SetProtectable(bool b) { protectable = b; } + bool IsProtected() const { return write_protected; } + void SetProtected(bool); + bool IsReadOnly() const { return read_only; } + void SetReadOnly(bool b) { read_only = b; } + bool IsStoppable() const { return stoppable; } + bool IsStopped() const { return stopped; } + bool IsRemovable() const { return removable; } + bool IsRemoved() const { return removed; } + void SetRemoved(bool b) { removed = b; } + bool IsLockable() const { return lockable; } + bool IsLocked() const { return locked; } + + virtual int GetId() const = 0; + int GetLun() const { return lun; } + + string GetVendor() const { return vendor; } + void SetVendor(const string&); + string GetProduct() const { return product; } + void SetProduct(const string&, bool = true); + string GetRevision() const { return revision; } + void SetRevision(const string&); + string GetPaddedName() const; + + bool SupportsParams() const { return supports_params; } + bool SupportsFile() const { return supports_file; } + void SupportsParams(bool b) { supports_params = b; } + void SupportsFile(bool b) { supports_file = b; } + auto GetParams() const { return params; } + virtual param_map GetDefaultParams() const { return {}; } + + void SetStatusCode(int s) { status_code = s; } + + bool Start(); + void Stop(); + virtual bool Eject(bool); +}; diff --git a/cpp/base/device_factory.cpp b/cpp/base/device_factory.cpp new file mode 100644 index 00000000..5c5325b7 --- /dev/null +++ b/cpp/base/device_factory.cpp @@ -0,0 +1,155 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/s2p_util.h" +#if defined BUILD_SCHD || defined BUILD_SCRM +#include "devices/scsi_hd.h" +#endif +#ifdef BUILD_SCMO +#include "devices/optical_memory.h" +#endif +#ifdef BUILD_SCCD +#include "devices/cd_dvd.h" +#endif +#ifdef BUILD_SCDP +#include "devices/daynaport.h" +#endif +#ifdef BUILD_SCLP +#include "devices/printer.h" +#endif +#ifdef BUILD_SCHS +#include "devices/host_services.h" +#endif +#ifdef BUILD_SAHD +#include "devices/sasi_hd.h" +#endif +#include "device_factory.h" + +using namespace std; +using namespace s2p_util; + +shared_ptr DeviceFactory::CreateDevice(PbDeviceType type, int lun, const string& filename) const +{ + // If no type was specified try to derive the device type from the filename + if (type == UNDEFINED) { + type = GetTypeForFile(filename); + if (type == UNDEFINED) { + return nullptr; + } + } + + shared_ptr device; + switch (type) { + +#if defined BUILD_SCHD || defined BUILD_SCRM + case SCHD: { + const string ext = GetExtensionLowerCase(filename); + device = make_shared(lun, false, ext == "hd1" ? scsi_level::scsi_1_ccs : scsi_level::scsi_2); + + // Some Apple tools require a particular drive identification. + // Except for the vendor string .hda is exactly the same as .hds. + if (ext == "hda") { + device->SetVendor("QUANTUM"); + device->SetProduct("FIREBALL"); + } + break; + } + + case SCRM: + device = make_shared(lun, true, scsi_level::scsi_2); + device->SetProduct("SCSI HD (REM.)"); + break; +#endif + +#ifdef BUILD_SCMO + case SCMO: + device = make_shared(lun); + device->SetProduct("SCSI MO"); + break; +#endif + +#ifdef BUILD_SCCD + case SCCD: + device = make_shared(lun, + GetExtensionLowerCase(filename) == "is1" ? scsi_level::scsi_1_ccs : scsi_level::scsi_2); + device->SetProduct("SCSI CD-ROM"); + break; +#endif + +#ifdef BUILD_SCDP + case SCDP: + device = make_shared(lun); + // Since this is an emulation for a specific device the full INQUIRY data have to be set accordingly + device->SetVendor("Dayna"); + device->SetProduct("SCSI/Link"); + device->SetRevision("1.4a"); + break; +#endif + +#ifdef BUILD_SCHS + case SCHS: + device = make_shared(lun); + device->SetProduct("Host Services"); + break; +#endif + +#ifdef BUILD_SCLP + case SCLP: + device = make_shared(lun); + device->SetProduct("SCSI PRINTER"); + break; +#endif + +#ifdef BUILD_SAHD + case SAHD: + device = make_shared(lun); + device->SetProduct("SASI HD"); + break; +#endif + + default: + break; + } + + return device; +} + +PbDeviceType DeviceFactory::GetTypeForFile(const string& filename) const +{ + const auto& mapping = GetExtensionMapping(); + if (const auto& it = mapping.find(GetExtensionLowerCase(filename)); it != mapping.end()) { + return it->second; + } + + if (const auto& it = DEVICE_MAPPING.find(filename); it != DEVICE_MAPPING.end()) { + return it->second; + } + + return UNDEFINED; +} + +unordered_map> DeviceFactory::GetExtensionMapping() const +{ + unordered_map> mapping; + +#if defined BUILD_SCHD | defined BUILD_SCRM + mapping["hd1"] = SCHD; + mapping["hds"] = SCHD; + mapping["hda"] = SCHD; + mapping["hdr"] = SCRM; +#endif +#ifdef BUILD_SCMO + mapping["mos"] = SCMO; +#endif +#ifdef BUILD_SCCD + mapping["is1"] = SCCD; + mapping["iso"] = SCCD; +#endif + + return mapping; +} diff --git a/cpp/base/device_factory.h b/cpp/base/device_factory.h new file mode 100644 index 00000000..9b2f4832 --- /dev/null +++ b/cpp/base/device_factory.h @@ -0,0 +1,42 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +// The DeviceFactory creates devices based on their type and the image file extension +// +//--------------------------------------------------------------------------- + +#pragma once + +#include +#include +#include "generated/command_interface.pb.h" +#include "shared/s2p_util.h" + +using namespace std; +using namespace command_interface; + +class PrimaryDevice; + +class DeviceFactory +{ + +public: + + DeviceFactory() = default; + ~DeviceFactory() = default; + + shared_ptr CreateDevice(PbDeviceType, int, const string&) const; + PbDeviceType GetTypeForFile(const string&) const; + unordered_map> GetExtensionMapping() const; + +private: + + const inline static unordered_map> DEVICE_MAPPING = { + { "daynaport", SCDP }, + { "printer", SCLP }, + { "services", SCHS } + }; +}; diff --git a/cpp/base/device_logger.cpp b/cpp/base/device_logger.cpp new file mode 100644 index 00000000..6f3ef692 --- /dev/null +++ b/cpp/base/device_logger.cpp @@ -0,0 +1,61 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "device_logger.h" + +using namespace std; +using namespace spdlog; + +void DeviceLogger::Trace(const string& message) const +{ + Log(level::trace, message); +} + +void DeviceLogger::Debug(const string& message) const +{ + Log(level::debug, message); +} + +void DeviceLogger::Info(const string& message) const +{ + Log(level::info, message); +} + +void DeviceLogger::Warn(const string& message) const +{ + Log(level::warn, message); +} + +void DeviceLogger::Error(const string& message) const +{ + Log(level::err, message); +} + +void DeviceLogger::Log(level::level_enum level, const string& message) const +{ + if ((log_device_id == -1 || log_device_id == id) && (lun == -1 || log_device_lun == -1 || log_device_lun == lun)) { + if (lun == -1) { + log(level, "(ID " + to_string(id) + ") - " + message); + } + else { + log(level, "(ID:LUN " + to_string(id) + ":" + to_string(lun) + ") - " + message); + } + } +} + +void DeviceLogger::SetIdAndLun(int i, int l) +{ + id = i; + lun = l; +} + +void DeviceLogger::SetLogIdAndLun(int i, int l) +{ + log_device_id = i; + log_device_lun = l; +} diff --git a/cpp/base/device_logger.h b/cpp/base/device_logger.h new file mode 100644 index 00000000..1c4d4ece --- /dev/null +++ b/cpp/base/device_logger.h @@ -0,0 +1,43 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "spdlog/spdlog.h" +#include + +using namespace std; + +class DeviceLogger +{ + +public: + + DeviceLogger() = default; + ~DeviceLogger() = default; + + void Trace(const string&) const; + void Debug(const string&) const; + void Info(const string&) const; + void Warn(const string&) const; + void Error(const string&) const; + + void SetIdAndLun(int, int); + static void SetLogIdAndLun(int, int); + +private: + + void Log(spdlog::level::level_enum, const string&) const; + + int id = -1; + int lun = -1; + + // TODO Try to only have one shared instance, so that these fields do not have to be static + static inline int log_device_id = -1; + static inline int log_device_lun = -1; +}; diff --git a/cpp/base/interfaces/scsi_block_commands.h b/cpp/base/interfaces/scsi_block_commands.h new file mode 100644 index 00000000..56d6536f --- /dev/null +++ b/cpp/base/interfaces/scsi_block_commands.h @@ -0,0 +1,29 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2022 Uwe Seimet +// +// Interface for SCSI block commands (see https://www.t10.org/drafts.htm, SBC-5) +// +//--------------------------------------------------------------------------- + +#pragma once + +class ScsiBlockCommands +{ + +public: + + ScsiBlockCommands() = default; + virtual ~ScsiBlockCommands() = default; + + // Mandatory commands + virtual void FormatUnit() = 0; + virtual void ReadCapacity10() = 0; + virtual void ReadCapacity16() = 0; + virtual void Read10() = 0; + virtual void Read16() = 0; + virtual void Write10() = 0; + virtual void Write16() = 0; +}; diff --git a/cpp/base/interfaces/scsi_mmc_commands.h b/cpp/base/interfaces/scsi_mmc_commands.h new file mode 100644 index 00000000..ff9c033e --- /dev/null +++ b/cpp/base/interfaces/scsi_mmc_commands.h @@ -0,0 +1,22 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2022 Uwe Seimet +// +// Interface for SCSI Multi-Media commands (see https://www.t10.org/drafts.htm, MMC-6) +// +//--------------------------------------------------------------------------- + +#pragma once + +class ScsiMmcCommands +{ + +public: + + ScsiMmcCommands() = default; + virtual ~ScsiMmcCommands() = default; + + virtual void ReadToc() = 0; +}; diff --git a/cpp/base/interfaces/scsi_primary_commands.h b/cpp/base/interfaces/scsi_primary_commands.h new file mode 100644 index 00000000..c012d078 --- /dev/null +++ b/cpp/base/interfaces/scsi_primary_commands.h @@ -0,0 +1,31 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2022 Uwe Seimet +// +// Interface for SCSI primary commands (see https://www.t10.org/drafts.htm, SPC-6) +// +//--------------------------------------------------------------------------- + +#pragma once + +class ScsiPrimaryCommands +{ + +public: + + ScsiPrimaryCommands() = default; + virtual ~ScsiPrimaryCommands() = default; + + // Mandatory commands + virtual void TestUnitReady() = 0; + virtual void Inquiry() = 0; + virtual void ReportLuns() = 0; + + // Optional commands implemented by all device types + virtual void RequestSense() = 0; + virtual void ReleaseUnit() = 0; + virtual void ReserveUnit() = 0; + virtual void SendDiagnostic() = 0; +}; diff --git a/cpp/base/interfaces/scsi_printer_commands.h b/cpp/base/interfaces/scsi_printer_commands.h new file mode 100644 index 00000000..37d2940e --- /dev/null +++ b/cpp/base/interfaces/scsi_printer_commands.h @@ -0,0 +1,26 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2022 Uwe Seimet +// +// Interface for SCSI printer commands (see SCSI-2 specification) +// +//--------------------------------------------------------------------------- + +#pragma once + +class ScsiPrinterCommands +{ + +public: + + ScsiPrinterCommands() = default; + virtual ~ScsiPrinterCommands() = default; + + // Mandatory commands + virtual void Print() = 0; + virtual void ReleaseUnit() = 0; + virtual void ReserveUnit() = 0; + virtual void SendDiagnostic() = 0; +}; diff --git a/cpp/base/primary_device.cpp b/cpp/base/primary_device.cpp new file mode 100644 index 00000000..bcabfd22 --- /dev/null +++ b/cpp/base/primary_device.cpp @@ -0,0 +1,312 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/shared_exceptions.h" +#include "scsi_command_util.h" +#include "primary_device.h" + +using namespace std; +using namespace scsi_defs; +using namespace scsi_command_util; + +bool PrimaryDevice::Init(const param_map& params) +{ + // Mandatory SCSI primary commands + AddCommand(scsi_command::cmd_test_unit_ready, [this] { TestUnitReady(); }); + AddCommand(scsi_command::cmd_inquiry, [this] { Inquiry(); }); + AddCommand(scsi_command::cmd_reportLuns, [this] { ReportLuns(); }); + + // Optional commands supported by all device types + AddCommand(scsi_command::cmd_request_sense, [this] { RequestSense(); }); + AddCommand(scsi_command::cmd_reserve6, [this] { ReserveUnit(); }); + AddCommand(scsi_command::cmd_release6, [this] { ReleaseUnit(); }); + AddCommand(scsi_command::cmd_send_diagnostic, [this] { SendDiagnostic(); }); + + SetParams(params); + + return true; +} + +void PrimaryDevice::AddCommand(scsi_command cmd, const operation& execute) +{ + commands[cmd] = execute; +} + +void PrimaryDevice::Dispatch(scsi_command cmd) +{ + if (const auto& it = commands.find(cmd); it != commands.end()) { + LogDebug(fmt::format("Device is executing {0} (${1:02x})", command_mapping.find(cmd)->second.second, + static_cast(cmd))); + + it->second(); + } + else { + LogTrace(fmt::format("Received unsupported command: {:02x}", static_cast(cmd))); + + throw scsi_exception(sense_key::illegal_request, asc::invalid_command_operation_code); + } +} + +void PrimaryDevice::Reset() +{ + DiscardReservation(); + + Device::Reset(); +} + +int PrimaryDevice::GetId() const +{ + return GetController() != nullptr ? GetController()->GetTargetId() : -1; +} + +void PrimaryDevice::SetController(AbstractController *c) +{ + controller = c; + + device_logger.SetIdAndLun(GetId(), GetLun()); +} + +void PrimaryDevice::TestUnitReady() +{ + CheckReady(); + + EnterStatusPhase(); +} + +void PrimaryDevice::Inquiry() +{ + // EVPD and page code check + if ((GetController()->GetCmdByte(1) & 0x01) || GetController()->GetCmdByte(2)) { + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } + + const vector buf = InquiryInternal(); + + const size_t allocation_length = min(buf.size(), static_cast(GetInt16(GetController()->GetCmd(), 3))); + + GetController()->CopyToBuffer(buf.data(), allocation_length); + + // Report if the device does not support the requested LUN + if (const int lun = GetController()->GetEffectiveLun(); !GetController()->HasDeviceForLun(lun)) { + LogTrace("LUN is not available"); + + // Signal that the requested LUN does not exist + GetController()->GetBuffer().data()[0] = 0x7f; + } + + EnterDataInPhase(); +} + +void PrimaryDevice::ReportLuns() +{ + // Only SELECT REPORT mode 0 is supported + if (GetController()->GetCmdByte(2)) { + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } + + const uint32_t allocation_length = GetInt32(GetController()->GetCmd(), 6); + + vector& buf = GetController()->GetBuffer(); + fill_n(buf.begin(), min(buf.size(), static_cast(allocation_length)), 0); + + uint32_t size = 0; + for (int lun = 0; lun < GetController()->GetMaxLuns(); lun++) { + if (GetController()->HasDeviceForLun(lun)) { + size += 8; + buf[size + 7] = (uint8_t)lun; + } + } + + SetInt16(buf, 2, size); + + size += 8; + + GetController()->SetLength(min(allocation_length, size)); + + EnterDataInPhase(); +} + +void PrimaryDevice::RequestSense() +{ + int lun = GetController()->GetEffectiveLun(); + + // Note: According to the SCSI specs the LUN handling for REQUEST SENSE non-existing LUNs do *not* result + // in CHECK CONDITION. Only the Sense Key and ASC are set in order to signal the non-existing LUN. + if (!GetController()->HasDeviceForLun(lun)) { + // LUN 0 can be assumed to be present (required to call RequestSense() below) + assert(GetController()->HasDeviceForLun(0)); + + lun = 0; + + // Do not raise an exception here because the rest of the code must be executed + GetController()->Error(sense_key::illegal_request, asc::invalid_lun); + + GetController()->SetStatus(status::good); + } + + vector buf = GetController()->GetDeviceForLun(lun)->HandleRequestSense(); + + const size_t allocation_length = min(buf.size(), static_cast(GetController()->GetCmdByte(4))); + + GetController()->CopyToBuffer(buf.data(), allocation_length); + + EnterDataInPhase(); +} + +void PrimaryDevice::SendDiagnostic() +{ + // Do not support PF bit + if (GetController()->GetCmdByte(1) & 0x10) { + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } + + // Do not support parameter list + if ((GetController()->GetCmdByte(3) != 0) || (GetController()->GetCmdByte(4) != 0)) { + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } + + EnterStatusPhase(); +} + +void PrimaryDevice::CheckReady() +{ + // Not ready if reset + if (IsReset()) { + SetReset(false); + LogTrace("Device in reset"); + throw scsi_exception(sense_key::unit_attention, asc::power_on_or_reset); + } + + // Not ready if it needs attention + if (IsAttn()) { + SetAttn(false); + LogTrace("Device in needs attention"); + throw scsi_exception(sense_key::unit_attention, asc::not_ready_to_ready_change); + } + + // Return status if not ready + if (!IsReady()) { + LogTrace("Device not ready"); + throw scsi_exception(sense_key::not_ready, asc::medium_not_present); + } + + LogTrace("Device is ready"); +} + +vector PrimaryDevice::HandleInquiry(device_type type, scsi_level level, bool is_removable) const +{ + vector buf(0x1F + 5); + + // Basic data + // buf[0] ... SCSI device type + // buf[1] ... Bit 7: Removable/not removable + // buf[2] ... SCSI compliance level of command system + // buf[3] ... SCSI compliance level of Inquiry response + // buf[4] ... Inquiry additional data + buf[0] = static_cast(type); + buf[1] = is_removable ? 0x80 : 0x00; + buf[2] = static_cast(level); + buf[3] = level >= scsi_level::scsi_2 ? + static_cast(scsi_level::scsi_2) : static_cast(scsi_level::scsi_1_ccs); + buf[4] = 0x1F; + + // Padded vendor, product, revision + memcpy(&buf.data()[8], GetPaddedName().c_str(), 28); + + return buf; +} + +vector PrimaryDevice::HandleRequestSense() const +{ + // Return not ready only if there are no errors + if (!GetStatusCode() && !IsReady()) { + throw scsi_exception(sense_key::not_ready, asc::medium_not_present); + } + + // Set 18 bytes including extended sense data + + vector buf(18); + + // Current error + buf[0] = (byte)0x70; + + buf[2] = (byte)(GetStatusCode() >> 16); + buf[7] = (byte)10; + buf[12] = (byte)(GetStatusCode() >> 8); + buf[13] = (byte)GetStatusCode(); + + LogTrace(fmt::format("Status ${0:x}, Sense Key ${1:x}, ASC ${2:x}", static_cast(GetController()->GetStatus()), + static_cast(buf[2]), static_cast(buf[12]))); + + return buf; +} + +bool PrimaryDevice::WriteByteSequence(span) +{ + LogError("Writing bytes is not supported by this device"); + + return false; +} + +void PrimaryDevice::ReserveUnit() +{ + reserving_initiator = GetController()->GetInitiatorId(); + + if (reserving_initiator != -1) { + LogTrace(fmt::format("Reserved device for initiator ID {}", reserving_initiator)); + } + else { + LogTrace("Reserved device for unknown initiator"); + } + + EnterStatusPhase(); +} + +void PrimaryDevice::ReleaseUnit() +{ + if (reserving_initiator != -1) { + LogTrace(fmt::format("Released device reserved by initiator ID {}", reserving_initiator)); + } + else { + LogTrace("Released device reserved by unknown initiator"); + } + + DiscardReservation(); + + EnterStatusPhase(); +} + +bool PrimaryDevice::CheckReservation(int initiator_id, scsi_command cmd, bool prevent_removal) const +{ + if (reserving_initiator == NOT_RESERVED || reserving_initiator == initiator_id) { + return true; + } + + // A reservation is valid for all commands except those excluded below + if (cmd == scsi_command::cmd_inquiry || cmd == scsi_command::cmd_request_sense || cmd == scsi_command::cmd_release6) { + return true; + } + // PREVENT ALLOW MEDIUM REMOVAL is permitted if the prevent bit is 0 + if (cmd == scsi_command::cmd_prevent_allow_medium_removal && !prevent_removal) { + return true; + } + + if (initiator_id != -1) { + LogTrace(fmt::format("Initiator ID {} tries to access reserved device", initiator_id)); + } + else { + LogTrace("Unknown initiator tries to access reserved device"); + } + + return false; +} + +void PrimaryDevice::DiscardReservation() +{ + reserving_initiator = NOT_RESERVED; +} diff --git a/cpp/base/primary_device.h b/cpp/base/primary_device.h new file mode 100644 index 00000000..50fa0bed --- /dev/null +++ b/cpp/base/primary_device.h @@ -0,0 +1,113 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +// A device implementing mandatory SCSI primary commands, to be used for subclassing +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "interfaces/scsi_primary_commands.h" +#include "controllers/abstract_controller.h" +#include "device.h" +#include "device_logger.h" +#include +#include +#include +#include + +using namespace std; +using namespace scsi_defs; + +class PrimaryDevice: private ScsiPrimaryCommands, public Device +{ + friend class AbstractController; + + using operation = function; + +public: + + PrimaryDevice(PbDeviceType type, int lun) : Device(type, lun) {} + ~PrimaryDevice() override = default; + + virtual bool Init(const param_map&); + virtual void CleanUp() { + // Override if cleanup work is required for a derived device + }; + + virtual void Dispatch(scsi_command); + + int GetId() const override; + + virtual bool WriteByteSequence(span); + + int GetSendDelay() const { return send_delay; } + + bool CheckReservation(int, scsi_command, bool) const; + void DiscardReservation(); + + void Reset() override; + + virtual void FlushCache() { + // Devices with a cache have to override this method + } + + // Devices providing statistics have to override this method + virtual vector GetStatistics() const { + return vector(); + } + +protected: + + void AddCommand(scsi_command, const operation&); + + vector HandleInquiry(scsi_defs::device_type, scsi_level, bool) const; + virtual vector InquiryInternal() const = 0; + void CheckReady(); + + void SetSendDelay(int s) { send_delay = s; } + + virtual void Inquiry() override; + virtual void RequestSense() override; + + void SendDiagnostic() override; + void ReserveUnit() override; + void ReleaseUnit() override; + + void EnterStatusPhase() const { controller->Status(); } + void EnterDataInPhase() const { controller->DataIn(); } + void EnterDataOutPhase() const { controller->DataOut(); } + + auto GetController() const { return controller; } + + void LogTrace(const string& s) const { device_logger.Trace(s); } + void LogDebug(const string& s) const { device_logger.Debug(s); } + void LogInfo(const string& s) const { device_logger.Info(s); } + void LogWarn(const string& s) const { device_logger.Warn(s); } + void LogError(const string& s) const { device_logger.Error(s); } + +private: + + static const int NOT_RESERVED = -2; + + void SetController(AbstractController *); + + void TestUnitReady() override; + void ReportLuns() override; + + vector HandleRequestSense() const; + + DeviceLogger device_logger; + + // Owned by the controller factory + AbstractController *controller = nullptr; + + unordered_map commands; + + int send_delay = BUS::SEND_NO_DELAY; + + int reserving_initiator = NOT_RESERVED; +}; diff --git a/cpp/base/scsi_command_util.cpp b/cpp/base/scsi_command_util.cpp new file mode 100644 index 00000000..30ba0296 --- /dev/null +++ b/cpp/base/scsi_command_util.cpp @@ -0,0 +1,139 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/shared_exceptions.h" +#include "scsi_command_util.h" +#include +#include + +using namespace scsi_defs; + +string scsi_command_util::ModeSelect(scsi_command cmd, cdb_t cdb, span buf, int length, int sector_size) +{ + assert(cmd == scsi_command::cmd_mode_select6 || cmd == scsi_command::cmd_mode_select10); + assert(length >= 0); + + string result; + + // PF + if (!(cdb[1] & 0x10)) { + // Vendor-specific parameters (SCSI-1) are not supported. + // Do not report an error in order to support Apple's HD SC Setup. + return result; + } + + // Skip block descriptors + int offset; + if (cmd == scsi_command::cmd_mode_select10) { + offset = 8 + GetInt16(buf, 6); + } + else { + offset = 4 + buf[3]; + } + length -= offset; + + bool has_valid_page_code = false; + + // Parse the pages + while (length > 0) { + // Format device page + if (const int page = buf[offset]; page == 0x03) { + if (length < 14) { + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_parameter_list); + } + + // With this page the sector size for a subsequent FORMAT can be selected, but only very few + // drives support this, e.g FUJITSU M2624S + // We are fine as long as the current sector size remains unchanged + if (GetInt16(buf, offset + 12) != sector_size) { + // With SCSI2Pi it is not possible to permanently (by formatting) change the sector size, + // because the size is an externally configurable setting only + spdlog::warn("In order to change the sector size use the -b option when launching s2p"); + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_parameter_list); + } + + has_valid_page_code = true; + } + else { + result = fmt::format("Unknown MODE SELECT page code: ${:02}", page); + } + + // Advance to the next page + const int size = buf[offset + 1] + 2; + + length -= size; + offset += size; + } + + if (!has_valid_page_code) { + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_parameter_list); + } + + return result; +} + +void scsi_command_util::EnrichFormatPage(map>& pages, bool changeable, int sector_size) +{ + if (changeable) { + // The sector size is simulated to be changeable, see the MODE SELECT implementation for details + SetInt16(pages[3], 12, sector_size); + } +} + +void scsi_command_util::AddAppleVendorModePage(map>& pages, bool changeable) +{ + // Page code 48 (30h) - Apple Vendor Mode Page + // Needed for SCCD for stock Apple driver support + // Needed for SCHD for stock Apple HD SC Setup + pages[48] = vector(30); + + // No changeable area + if (!changeable) { + constexpr const char APPLE_DATA[] = "APPLE COMPUTER, INC "; + memcpy(&pages[48].data()[2], APPLE_DATA, sizeof(APPLE_DATA)); + } +} + +int scsi_command_util::GetInt24(span buf, int offset) +{ + assert(buf.size() > static_cast(offset) + 2); + + return (buf[offset] << 16) | (buf[offset + 1] << 8) | buf[offset + 2]; +} + +uint32_t scsi_command_util::GetInt32(span buf, int offset) +{ + assert(buf.size() > static_cast(offset) + 3); + + return (static_cast(buf[offset]) << 24) | (static_cast(buf[offset + 1]) << 16) | + (static_cast(buf[offset + 2]) << 8) | static_cast(buf[offset + 3]); +} + +uint64_t scsi_command_util::GetInt64(span buf, int offset) +{ + assert(buf.size() > static_cast(offset) + 7); + + return (static_cast(buf[offset]) << 56) | (static_cast(buf[offset + 1]) << 48) | + (static_cast(buf[offset + 2]) << 40) | (static_cast(buf[offset + 3]) << 32) | + (static_cast(buf[offset + 4]) << 24) | (static_cast(buf[offset + 5]) << 16) | + (static_cast(buf[offset + 6]) << 8) | static_cast(buf[offset + 7]); +} + +void scsi_command_util::SetInt64(vector& buf, int offset, uint64_t value) +{ + assert(buf.size() > static_cast(offset) + 7); + + buf[offset] = static_cast(value >> 56); + buf[offset + 1] = static_cast(value >> 48); + buf[offset + 2] = static_cast(value >> 40); + buf[offset + 3] = static_cast(value >> 32); + buf[offset + 4] = static_cast(value >> 24); + buf[offset + 5] = static_cast(value >> 16); + buf[offset + 6] = static_cast(value >> 8); + buf[offset + 7] = static_cast(value); +} diff --git a/cpp/base/scsi_command_util.h b/cpp/base/scsi_command_util.h new file mode 100644 index 00000000..79819da8 --- /dev/null +++ b/cpp/base/scsi_command_util.h @@ -0,0 +1,59 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +// Shared code for SCSI command implementations +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "shared/scsi.h" +#include +#include +#include +#include +#include + +using namespace std; + +namespace scsi_command_util +{ + string ModeSelect(scsi_defs::scsi_command, cdb_t, span, int, int); + void EnrichFormatPage(map>&, bool, int); + void AddAppleVendorModePage(map>&, bool); + + int GetInt16(const auto buf, int offset) + { + assert(buf.size() > static_cast(offset) + 1); + + return (static_cast(buf[offset]) << 8) | buf[offset + 1]; + }; + + template + void SetInt16(vector& buf, int offset, int value) + { + assert(buf.size() > static_cast(offset) + 1); + + buf[offset] = static_cast(value >> 8); + buf[offset + 1] = static_cast(value); + } + + template + void SetInt32(vector& buf, int offset, uint32_t value) + { + assert(buf.size() > static_cast(offset) + 3); + + buf[offset] = static_cast(value >> 24); + buf[offset + 1] = static_cast(value >> 16); + buf[offset + 2] = static_cast(value >> 8); + buf[offset + 3] = static_cast(value); + } + + int GetInt24(span, int); + uint32_t GetInt32(span , int); + uint64_t GetInt64(span, int); + void SetInt64(vector&, int, uint64_t); +} diff --git a/cpp/buses/bus.cpp b/cpp/buses/bus.cpp new file mode 100644 index 00000000..b4216d40 --- /dev/null +++ b/cpp/buses/bus.cpp @@ -0,0 +1,84 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +// Copyright (C) 2014-2020 GIMONS +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "bus.h" + +using namespace std; +using namespace scsi_defs; + +int BUS::GetCommandByteCount(uint8_t opcode) +{ + const auto& mapping = command_mapping.find(static_cast(opcode)); + + return mapping != command_mapping.end() ? mapping->second.first : 0; +} + +phase_t BUS::GetPhase() +{ + Acquire(); + + if (GetSEL()) { + return phase_t::selection; + } + + if (!GetBSY()) { + return phase_t::busfree; + } + + // Get target phase from bus signal line + int mci = GetMSG() ? 0b100 : 0b000; + mci |= GetCD() ? 0b010 : 0b000; + mci |= GetIO() ? 0b001 : 0b000; + return GetPhase(mci); +} + +string BUS::GetPhaseName(phase_t phase) { + const auto& it = phase_names.find(phase); + return it != phase_names.end() ? it->second : "????"; +} + +// Phase Table +// Reference Table 8: https://www.staff.uni-mainz.de/tacke/scsi/SCSI2-06.html +// This determines the phase based upon the Msg, C/D and I/O signals. +// +// |MSG|C/D|I/O| Phase +// | 0 | 0 | 0 | DATA OUT +// | 0 | 0 | 1 | DATA IN +// | 0 | 1 | 0 | COMMAND +// | 0 | 1 | 1 | STATUS +// | 1 | 0 | 0 | RESERVED +// | 1 | 0 | 1 | RESERVED +// | 1 | 1 | 0 | MESSAGE OUT +// | 1 | 1 | 1 | MESSAGE IN +// +const array BUS::phases = { + phase_t::dataout, + phase_t::datain, + phase_t::command, + phase_t::status, + phase_t::reserved, + phase_t::reserved, + phase_t::msgout, + phase_t::msgin +}; + +const unordered_map BUS::phase_names = { + { phase_t::busfree, "busfree" }, + { phase_t::arbitration, "arbitration" }, + { phase_t::selection, "selection" }, + { phase_t::reselection, "reselection" }, + { phase_t::command, "command" }, + { phase_t::datain, "datain" }, + { phase_t::dataout, "dataout" }, + { phase_t::status, "status" }, + { phase_t::msgin, "msgin" }, + { phase_t::msgout, "msgout" }, + { phase_t::reserved, "reserved" } +}; diff --git a/cpp/buses/bus.h b/cpp/buses/bus.h new file mode 100644 index 00000000..4c437dc6 --- /dev/null +++ b/cpp/buses/bus.h @@ -0,0 +1,99 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +// Copyright (C) 2014-2020 GIMONS +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "buses/pin_control.h" +#include "shared/scsi.h" +#include +#include +#include +#include +#include +#include + +#ifdef __linux__ +// Check SEL signal by event +#define USE_SEL_EVENT_ENABLE +#endif + +// Do not disable IRQs +//#define NO_IRQ_DISABLE + +using namespace std; +using namespace scsi_defs; + +// SCSI Bus timings taken from https://www.staff.uni-mainz.de/tacke/scsi/SCSI2-05.html +const static int SCSI_DELAY_ARBITRATION_DELAY_NS = 2400; +const static int SCSI_DELAY_ASSERTION_PERIOD_NS = 90; +const static int SCSI_DELAY_BUS_CLEAR_DELAY_NS = 800; +const static int SCSI_DELAY_BUS_FREE_DELAY_NS = 800; +const static int SCSI_DELAY_BUS_SET_DELAY_NS = 1800; +const static int SCSI_DELAY_BUS_SETTLE_DELAY_NS = 400; +const static int SCSI_DELAY_CABLE_SKEW_DELAY_NS = 10; +const static int SCSI_DELAY_DATA_RELEASE_DELAY_NS = 400; +const static int SCSI_DELAY_DESKEW_DELAY_NS = 45; +const static int SCSI_DELAY_DISCONNECTION_DELAY_US = 200; +const static int SCSI_DELAY_HOLD_TIME_NS = 45; +const static int SCSI_DELAY_NEGATION_PERIOD_NS = 90; +const static int SCSI_DELAY_POWER_ON_TO_SELECTION_TIME_S = 10; // (recommended) +const static int SCSI_DELAY_RESET_TO_SELECTION_TIME_US = 250 * 1000; // (recommended) +const static int SCSI_DELAY_RESET_HOLD_TIME_US = 25; +const static int SCSI_DELAY_SELECTION_ABORT_TIME_US = 200; +const static int SCSI_DELAY_SELECTION_TIMEOUT_DELAY_NS = 250 * 1000; // (recommended) + +// The DaynaPort SCSI Link do a short delay in the middle of transfering +// a packet. This is the number of uS that will be delayed between the +// header and the actual data. +const static int SCSI_DELAY_SEND_DATA_DAYNAPORT_NS = 100'000; + +class BUS : public PinControl +{ + +public: + + // Operation modes + enum class mode_e { + TARGET = 0, + INITIATOR = 1 + }; + + static int GetCommandByteCount(uint8_t); + + virtual bool Init(mode_e) = 0; + virtual void Reset() = 0; + virtual void CleanUp() = 0; + + phase_t GetPhase(); + static phase_t GetPhase(int mci) + { + return phases[mci]; + } + static string GetPhaseName(phase_t); + + virtual uint32_t Acquire() = 0; + virtual int CommandHandShake(vector &) = 0; + virtual int ReceiveHandShake(uint8_t *, int) = 0; + virtual int SendHandShake(uint8_t *, int, int = SEND_NO_DELAY) = 0; + + virtual bool WaitForSelectEvent() = 0; + + virtual bool GetSignal(int) const = 0; + virtual void SetSignal(int, bool) = 0; + + // Work-around needed for the DaynaPort emulation + static const int SEND_NO_DELAY = -1; + +private: + + static const array phases; + + static const unordered_map phase_names; +}; diff --git a/cpp/buses/bus_factory.cpp b/cpp/buses/bus_factory.cpp new file mode 100644 index 00000000..0d741771 --- /dev/null +++ b/cpp/buses/bus_factory.cpp @@ -0,0 +1,66 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "bus_factory.h" +#include "buses/gpiobus_raspberry.h" +#include +#include +#include + +using namespace std; + +unique_ptr BusFactory::CreateBus(BUS::mode_e mode, bool in_process) +{ + unique_ptr bus; + + if (in_process) { + bus = make_unique(in_process_bus, true); + } + else { + if (CheckForPi()) { + if (getuid()) { + spdlog::error("GPIO bus access requires root permissions"); + return nullptr; + } + + bus = make_unique(); + } else { + bus = make_unique(in_process_bus, false); + } + } + + if (bus && bus->Init(mode)) { + bus->Reset(); + } + + return bus; +} + +bool BusFactory::CheckForPi() +{ + ifstream in(DEVICE_TREE_MODEL_PATH); + if (in.fail()) { + spdlog::info("This platform does not appear to be a Raspberry Pi, functionality is limited"); + return false; + } + + stringstream s; + s << in.rdbuf(); + const string model = s.str(); + + if (model.starts_with("Raspberry Pi") && !model.starts_with("Raspberry Pi 5")) { + spdlog::info("Detected {}", model); + is_raspberry_pi = true; + return true; + } + + spdlog::error("Unsupported Raspberry Pi model '{}', functionality may be limited", model); + + return false; +} + diff --git a/cpp/buses/bus_factory.h b/cpp/buses/bus_factory.h new file mode 100644 index 00000000..534b9aef --- /dev/null +++ b/cpp/buses/bus_factory.h @@ -0,0 +1,38 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "buses/in_process_bus.h" +#include + +using namespace std; + +class BusFactory +{ + +public: + + BusFactory() = default; + ~BusFactory() = default; + + unique_ptr CreateBus(BUS::mode_e, bool = false); + + bool IsRaspberryPi() const { return is_raspberry_pi; } + +private: + + bool CheckForPi(); + + // Bus instance shared by initiator and target + inline static InProcessBus in_process_bus; + + bool is_raspberry_pi = false; + + inline static const string DEVICE_TREE_MODEL_PATH = "/proc/device-tree/model"; +}; diff --git a/cpp/buses/connection_type/connection_aibom.h b/cpp/buses/connection_type/connection_aibom.h new file mode 100644 index 00000000..ca8d268e --- /dev/null +++ b/cpp/buses/connection_type/connection_aibom.h @@ -0,0 +1,55 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Powered by XM6 TypeG Technology. +// Copyright (C) 2016-2020 GIMONS +// +//--------------------------------------------------------------------------- + +#pragma once + +#include + +// +// RaSCSI Adapter Aibom version +// + +const std::string CONNECT_DESC = "AIBOM PRODUCTS version"; // Startup message + +// Select signal control mode +const static int SIGNAL_CONTROL_MODE = 2; // SCSI positive logic specification + +// Control signal output logic +#define ACT_ON ON // ACTIVE SIGNAL ON +#define ENB_ON ON // ENABLE SIGNAL ON +#define IND_IN OFF // INITIATOR SIGNAL INPUT +#define TAD_IN OFF // TARGET SIGNAL INPUT +#define DTD_IN OFF // DATA SIGNAL INPUT + +// Control signal pin assignment (-1 means no control) +const static int PIN_ACT = 4; // ACTIVE +const static int PIN_ENB = 17; // ENABLE +const static int PIN_IND = 27; // INITIATOR CTRL DIRECTION +const static int PIN_TAD = -1; // TARGET CTRL DIRECTION +const static int PIN_DTD = 18; // DATA DIRECTION + +// SCSI signal pin assignment +const static int PIN_DT0 = 6; // Data 0 +const static int PIN_DT1 = 12; // Data 1 +const static int PIN_DT2 = 13; // Data 2 +const static int PIN_DT3 = 16; // Data 3 +const static int PIN_DT4 = 19; // Data 4 +const static int PIN_DT5 = 20; // Data 5 +const static int PIN_DT6 = 26; // Data 6 +const static int PIN_DT7 = 21; // Data 7 +const static int PIN_DP = 5; // Data parity +const static int PIN_ATN = 22; // ATN +const static int PIN_RST = 25; // RST +const static int PIN_ACK = 10; // ACK +const static int PIN_REQ = 7; // REQ +const static int PIN_MSG = 9; // MSG +const static int PIN_CD = 11; // CD +const static int PIN_IO = 23; // IO +const static int PIN_BSY = 24; // BSY +const static int PIN_SEL = 8; // SEL diff --git a/cpp/buses/connection_type/connection_fullspec.h b/cpp/buses/connection_type/connection_fullspec.h new file mode 100644 index 00000000..15c73c18 --- /dev/null +++ b/cpp/buses/connection_type/connection_fullspec.h @@ -0,0 +1,55 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Powered by XM6 TypeG Technology. +// Copyright (C) 2016-2020 GIMONS +// +//--------------------------------------------------------------------------- + +#pragma once + +#include + +// +// PiSCSI standard (SCSI logic, standard pin assignment) +// + +const std::string CONNECT_DESC = "FULLSPEC"; // Startup message + +// Select signal control mode +const static int SIGNAL_CONTROL_MODE = 0; // SCSI logical specification + +// Control signal pin assignment (-1 means no control) +const static int PIN_ACT = 4; // ACTIVE +const static int PIN_ENB = 5; // ENABLE +const static int PIN_IND = 6; // INITIATOR CTRL DIRECTION +const static int PIN_TAD = 7; // TARGET CTRL DIRECTION +const static int PIN_DTD = 8; // DATA DIRECTION + +// Control signal output logic +#define ACT_ON ON // ACTIVE SIGNAL ON +#define ENB_ON ON // ENABLE SIGNAL ON +#define IND_IN OFF // INITIATOR SIGNAL INPUT +#define TAD_IN OFF // TARGET SIGNAL INPUT +#define DTD_IN ON // DATA SIGNAL INPUT + +// SCSI signal pin assignment +const static int PIN_DT0 = 10; // Data 0 +const static int PIN_DT1 = 11; // Data 1 +const static int PIN_DT2 = 12; // Data 2 +const static int PIN_DT3 = 13; // Data 3 +const static int PIN_DT4 = 14; // Data 4 +const static int PIN_DT5 = 15; // Data 5 +const static int PIN_DT6 = 16; // Data 6 +const static int PIN_DT7 = 17; // Data 7 +const static int PIN_DP = 18; // Data parity +const static int PIN_ATN = 19; // ATN +const static int PIN_RST = 20; // RST +const static int PIN_ACK = 21; // ACK +const static int PIN_REQ = 22; // REQ +const static int PIN_MSG = 23; // MSG +const static int PIN_CD = 24; // CD +const static int PIN_IO = 25; // IO +const static int PIN_BSY = 26; // BSY +const static int PIN_SEL = 27; // SEL diff --git a/cpp/buses/connection_type/connection_gamernium.h b/cpp/buses/connection_type/connection_gamernium.h new file mode 100644 index 00000000..b571cce8 --- /dev/null +++ b/cpp/buses/connection_type/connection_gamernium.h @@ -0,0 +1,55 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Powered by XM6 TypeG Technology. +// Copyright (C) 2016-2020 GIMONS +// +//--------------------------------------------------------------------------- + +#pragma once + +#include + +// +// RaSCSI Adapter GAMERnium.com version +// + +const std::string CONNECT_DESC = "GAMERnium.com version"; // Startup message + +// Select signal control mode +const static int SIGNAL_CONTROL_MODE = 0; // SCSI logical specification + +// Control signal output logic +#define ACT_ON ON // ACTIVE SIGNAL ON +#define ENB_ON ON // ENABLE SIGNAL ON +#define IND_IN OFF // INITIATOR SIGNAL INPUT +#define TAD_IN OFF // TARGET SIGNAL INPUT +#define DTD_IN ON // DATA SIGNAL INPUT + +// Control signal pin assignment (-1 means no control) +const static int PIN_ACT = 14; // ACTIVE +const static int PIN_ENB = 6; // ENABLE +const static int PIN_IND = 7; // INITIATOR CTRL DIRECTION +const static int PIN_TAD = 8; // TARGET CTRL DIRECTION +const static int PIN_DTD = 5; // DATA DIRECTION + +// SCSI signal pin assignment +const static int PIN_DT0 = 21; // Data 0 +const static int PIN_DT1 = 26; // Data 1 +const static int PIN_DT2 = 20; // Data 2 +const static int PIN_DT3 = 19; // Data 3 +const static int PIN_DT4 = 16; // Data 4 +const static int PIN_DT5 = 13; // Data 5 +const static int PIN_DT6 = 12; // Data 6 +const static int PIN_DT7 = 11; // Data 7 +const static int PIN_DP = 25; // Data parity +const static int PIN_ATN = 10; // ATN +const static int PIN_RST = 22; // RST +const static int PIN_ACK = 24; // ACK +const static int PIN_REQ = 15; // REQ +const static int PIN_MSG = 17; // MSG +const static int PIN_CD = 18; // CD +const static int PIN_IO = 4; // IO +const static int PIN_BSY = 27; // BSY +const static int PIN_SEL = 23; // SEL diff --git a/cpp/buses/connection_type/connection_standard.h b/cpp/buses/connection_type/connection_standard.h new file mode 100644 index 00000000..a493ffa3 --- /dev/null +++ b/cpp/buses/connection_type/connection_standard.h @@ -0,0 +1,55 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Powered by XM6 TypeG Technology. +// Copyright (C) 2016-2020 GIMONS +// +//--------------------------------------------------------------------------- + +#pragma once + +#include + +// +// PiSCSI standard (SCSI logic, standard pin assignment) +// + +const std::string CONNECT_DESC = "STANDARD"; // Startup message + +// Select signal control mode +const static int SIGNAL_CONTROL_MODE = 0; // SCSI logical specification + +// Control signal pin assignment (-1 means no control) +const static int PIN_ACT = 4; // ACTIVE +const static int PIN_ENB = 5; // ENABLE +const static int PIN_IND = -1; // INITIATOR CTRL DIRECTION +const static int PIN_TAD = -1; // TARGET CTRL DIRECTION +const static int PIN_DTD = -1; // DATA DIRECTION + +// Control signal output logic +#define ACT_ON ON // ACTIVE SIGNAL ON +#define ENB_ON ON // ENABLE SIGNAL ON +#define IND_IN OFF // INITIATOR SIGNAL INPUT +#define TAD_IN OFF // TARGET SIGNAL INPUT +#define DTD_IN ON // DATA SIGNAL INPUT + +// SCSI signal pin assignment +const static int PIN_DT0 = 10; // Data 0 +const static int PIN_DT1 = 11; // Data 1 +const static int PIN_DT2 = 12; // Data 2 +const static int PIN_DT3 = 13; // Data 3 +const static int PIN_DT4 = 14; // Data 4 +const static int PIN_DT5 = 15; // Data 5 +const static int PIN_DT6 = 16; // Data 6 +const static int PIN_DT7 = 17; // Data 7 +const static int PIN_DP = 18; // Data parity +const static int PIN_ATN = 19; // ATN +const static int PIN_RST = 20; // RST +const static int PIN_ACK = 21; // ACK +const static int PIN_REQ = 22; // REQ +const static int PIN_MSG = 23; // MSG +const static int PIN_CD = 24; // CD +const static int PIN_IO = 25; // IO +const static int PIN_BSY = 26; // BSY +const static int PIN_SEL = 27; // SEL diff --git a/cpp/buses/gpiobus.cpp b/cpp/buses/gpiobus.cpp new file mode 100644 index 00000000..63dc3d74 --- /dev/null +++ b/cpp/buses/gpiobus.cpp @@ -0,0 +1,298 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Powered by XM6 TypeG Technology. +// Copyright (C) 2016-2020 GIMONS +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "buses/gpiobus.h" +#ifdef __linux__ +#include +#endif +#include + +using namespace std; + +bool GPIOBUS::Init(mode_e mode) +{ + operation_mode = mode; + + return true; +} + +int GPIOBUS::CommandHandShake(vector &buf) +{ + assert(operation_mode == mode_e::TARGET); + + DisableIRQ(); + + SetREQ(true); + + bool ack = WaitACK(true); + + buf[0] = GetDAT(); + + SetREQ(false); + + // Timeout waiting for ACK to change + if (!ack || !WaitACK(false)) { + EnableIRQ(); + return 0; + } + + // The ICD AdSCSI ST, AdSCSI Plus ST and AdSCSI Micro ST host adapters allow SCSI devices to be connected + // to the ACSI bus of Atari ST/TT computers and some clones. ICD-aware drivers prepend a $1F byte in front + // of the CDB (effectively resulting in a custom SCSI command) in order to get access to the full SCSI + // command set. Native ACSI is limited to the low SCSI command classes with command bytes < $20. + // Most other host adapters (e.g. LINK96/97 and the one by Inventronik) and also several devices (e.g. + // UltraSatan or GigaFile) that can directly be connected to the Atari's ACSI port also support ICD + // semantics. In fact, these semantics have become a standard in the Atari world. + + // SCSi2Pi becomes ICD compatible by ignoring the prepended $1F byte before processing the CDB. + if (buf[0] == 0x1f) { + SetREQ(true); + + ack = WaitACK(true); + + // Get the actual SCSI command + buf[0] = GetDAT(); + + SetREQ(false); + + // Timeout waiting for ACK to change + if (!ack || !WaitACK(false)) { + EnableIRQ(); + return 0; + } + } + + const int command_byte_count = GetCommandByteCount(buf[0]); + if (!command_byte_count) { + EnableIRQ(); + + // Unknown command + return 0; + } + + int offset = 0; + + int bytes_received; + for (bytes_received = 1; bytes_received < command_byte_count; bytes_received++) { + ++offset; + + SetREQ(true); + + ack = WaitACK(true); + + buf[offset] = GetDAT(); + + SetREQ(false); + + // Timeout waiting for ACK to change + if (!ack || !WaitACK(false)) { + break; + } + } + + EnableIRQ(); + + return bytes_received; +} + +// Handshake for DATA IN and MESSAGE IN +int GPIOBUS::ReceiveHandShake(uint8_t *buf, int count) +{ + int bytes_received; + + DisableIRQ(); + + if (operation_mode == mode_e::TARGET) { + for (bytes_received = 0; bytes_received < count; bytes_received++) { + SetREQ(true); + + const bool ack = WaitACK(true); + + *buf = GetDAT(); + + SetREQ(false); + + // Timeout waiting for ACK to change + if (!ack || !WaitACK(false)) { + break; + } + + buf++; + } + } else { + const phase_t phase = GetPhase(); + + for (bytes_received = 0; bytes_received < count; bytes_received++) { + // Check for timeout waiting for REQ signal + if (!WaitREQ(true)) { + break; + } + + // Phase error + // TODO Assumption: Phase does not change here, but only below + if (GetPhase() != phase) { + break; + } + + *buf = GetDAT(); + + SetACK(true); + + const bool req = WaitREQ(false); + + SetACK(false); + + // Check for timeout waiting for REQ to clear and for unexpected phase change + if (!req || GetPhase() != phase) { + break; + } + + buf++; + } + } + + EnableIRQ(); + + return bytes_received; +} + +// Handshake for DATA OUT and MESSAGE OUT +int GPIOBUS::SendHandShake(uint8_t *buf, int count, int daynaport_delay_after_bytes) +{ + int bytes_sent; + + DisableIRQ(); + + if (operation_mode == mode_e::TARGET) { + for (bytes_sent = 0; bytes_sent < count; bytes_sent++) { + if (bytes_sent == daynaport_delay_after_bytes) { + EnableIRQ(); + const timespec ts = { .tv_sec = 0, .tv_nsec = SCSI_DELAY_SEND_DATA_DAYNAPORT_NS}; + nanosleep(&ts, nullptr); + DisableIRQ(); + } + + SetDAT(*buf); + + // Check for timeout waiting for ACK to clear + if (!WaitACK(false)) { + break; + } + + SetREQ(true); + + const bool ack = WaitACK(true); + + SetREQ(false); + + // Check for timeout waiting for ACK to clear + // TODO Do we really have to clear REQ here and everywhere else before checking this? + if (!ack) { + break; + } + + buf++; + } + + WaitACK(false); + } else { + const phase_t phase = GetPhase(); + + for (bytes_sent = 0; bytes_sent < count; bytes_sent++) { + SetDAT(*buf); + + // Check for timeout waiting for REQ to be asserted + if (!WaitREQ(true)) { + break; + } + + // Signal the last MESSAGE OUT byte + if (phase == phase_t::msgout && bytes_sent == count - 1) { + SetATN(false); + } + + // Phase error + // TODO Assumption: Phase does not change here, but only below + if (GetPhase() != phase) { + break; + } + + SetACK(true); + + const bool req = WaitREQ(false); + + SetACK(false); + + // Check for timeout waiting for REQ to clear and for unexpected phase change + if (!req || GetPhase() != phase) { + break; + } + + buf++; + } + } + + EnableIRQ(); + + return bytes_sent; +} + +bool GPIOBUS::WaitForSelectEvent() +{ +#ifndef USE_SEL_EVENT_ENABLE + Acquire(); + if (!GetSEL()) { + const timespec ts = { .tv_sec = 0, .tv_nsec = 0}; + nanosleep(&ts, nullptr); + return false; + } +#else + errno = 0; + + if (epoll_event epev; epoll_wait(epfd, &epev, 1, -1) <= 0) { + if (errno != EINTR) { + spdlog::warn("epoll_wait failed"); + } + return false; + } + + if (gpioevent_data gpev; read(selevreq.fd, &gpev, sizeof(gpev)) < 0) { + if (errno != EINTR) { + spdlog::warn("read failed"); + } + return false; + } + + Acquire(); +#endif + + return true; +} + +bool GPIOBUS::WaitSignal(int pin, bool ast) +{ + const auto now = chrono::steady_clock::now(); + + // Wait for up to 3 s + do { + Acquire(); + + if (GetSignal(pin) == ast) { + return true; + } + + // Abort on a reset + if (GetRST()) { + return false; + } + } while ((chrono::duration_cast(chrono::steady_clock::now() - now).count()) < 3); + + return false; +} diff --git a/cpp/buses/gpiobus.h b/cpp/buses/gpiobus.h new file mode 100644 index 00000000..e9828f81 --- /dev/null +++ b/cpp/buses/gpiobus.h @@ -0,0 +1,191 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Powered by XM6 TypeG Technology. +// Copyright (C) 2016-2020 GIMONS +// Copyright (C) Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "buses/bus.h" +#include +#include +#include + +#ifdef __linux__ +#include +#endif + +//--------------------------------------------------------------------------- +// +// Connection method definitions +// +//--------------------------------------------------------------------------- +//#define CONNECT_TYPE_STANDARD // Standard (SCSI logic, standard pin assignment) +//#define CONNECT_TYPE_FULLSPEC // Full spec (SCSI logic, standard pin assignment) +//#define CONNECT_TYPE_AIBOM // AIBOM version (positive logic, unique pin assignment) +//#define CONNECT_TYPE_GAMERNIUM // GAMERnium.com version (standard logic, unique pin assignment) + +#if defined CONNECT_TYPE_STANDARD +#include "buses/connection_type/connection_standard.h" +#elif defined CONNECT_TYPE_FULLSPEC +#include "buses/connection_type/connection_fullspec.h" +#elif defined CONNECT_TYPE_AIBOM +#include "buses/connection_type/connection_aibom.h" +#elif defined CONNECT_TYPE_GAMERNIUM +#include "buses/connection_type/connection_gamernium.h" +#else +#error Invalid connection type or none specified +#endif + +using namespace std; + +//--------------------------------------------------------------------------- +// +// Signal control logic and pin assignment customization +// +//--------------------------------------------------------------------------- + +//--------------------------------------------------------------------------- +// +// SIGNAL_CONTROL_MODE: Signal control mode selection +// You can customize the signal control logic from Version 1.22 +// +// 0:SCSI logical specification +// Conversion board using 74LS641-1 etc. directly connected or published on HP +// True : 0V +// False : Open collector output (disconnect from bus) +// +// 1:Negative logic specification (when using conversion board for negative logic -> SCSI logic) +// There is no conversion board with this specification at this time +// True : 0V -> (CONVERT) -> 0V +// False : 3.3V -> (CONVERT) -> Open collector output +// +// 2:Positive logic specification (when using the conversion board for positive logic -> SCSI logic) +// PiSCSI Adapter Rev.C @132sync etc. +// +// True : 3.3V -> (CONVERT) -> 0V +// False : 0V -> (CONVERT) -> Open collector output +// +//--------------------------------------------------------------------------- + +//--------------------------------------------------------------------------- +// +// Control signal pin assignment setting +// GPIO pin mapping table for control signals. +// +// Control signal: +// PIN_ACT +// Signal that indicates the status of processing SCSI command. +// PIN_ENB +// Signal that indicates the valid signal from start to finish. +// PIN_TAD +// Signal that indicates the input/output direction of the target signal (BSY,IO,CD,MSG,REG). +// PIN_IND +// Signal that indicates the input/output direction of the initiator signal (SEL, ATN, RST, ACK). +// PIN_DTD +// Signal that indicates the input/output direction of the data lines (DT0...DT7,DP). +// +//--------------------------------------------------------------------------- + +//--------------------------------------------------------------------------- +// +// Control signal output logic +// 0V:FALSE 3.3V:TRUE +// +// ACT_ON +// PIN_ACT signal +// ENB_ON +// PIN_ENB signal +// TAD_IN +// PIN_TAD This is the logic when inputting. +// IND_IN +// PIN_ENB This is the logic when inputting. +// DTD_IN +// PIN_ENB This is the logic when inputting. +// +//--------------------------------------------------------------------------- + +//--------------------------------------------------------------------------- +// +// SCSI signal pin assignment setting +// GPIO pin mapping table for SCSI signals. +// PIN_DT0~PIN_SEL +// +//--------------------------------------------------------------------------- + +// Constant declarations (GPIO) +const static int GPIO_INPUT = 0; +const static int GPIO_OUTPUT = 1; +const static int GPIO_IRQ_IN = 3; +const static int GPIO_PULLNONE = 0; +const static int GPIO_PULLDOWN = 1; +const static int GPIO_PULLUP = 2; + +// Constant declarations (Control signals) +#define ACT_OFF !ACT_ON +#define ENB_OFF !ENB_ON +#define TAD_OUT !TAD_IN +#define IND_OUT !IND_IN +#define DTD_OUT !DTD_IN + +// Constant declarations (SCSI) +#define IN GPIO_INPUT +#define OUT GPIO_OUTPUT +const static int ON = 1; +const static int OFF = 0; + +class GPIOBUS : public BUS +{ + +public: + + GPIOBUS() = default; + ~GPIOBUS() override = default; + + bool Init(mode_e mode = mode_e::TARGET) override; + + int CommandHandShake(vector&) override; + int ReceiveHandShake(uint8_t *, int) override; + int SendHandShake(uint8_t *, int, int = SEND_NO_DELAY) override; + + bool WaitForSelectEvent() override; + +protected: + + virtual void MakeTable() = 0; + + virtual bool WaitSignal(int, bool); + + virtual bool WaitREQ(bool) = 0; + virtual bool WaitACK(bool) = 0; + + virtual void EnableIRQ() = 0; + virtual void DisableIRQ() = 0; + + // Set GPIO output signal + virtual void PinSetSignal(int, bool) = 0; + + // Set GPIO drive strength + virtual void SetSignalDriveStrength(uint32_t) = 0; + +#ifdef USE_SEL_EVENT_ENABLE + // SEL signal event request + struct gpioevent_request selevreq = {}; + // epoll file descriptor + int epfd = 0; +#endif + + mode_e operation_mode = mode_e::TARGET; + +private: + + // The DaynaPort SCSI Link do a short delay in the middle of transfering + // a packet. This is the number of uS that will be delayed between the + // header and the actual data. + // TODO Should be verified + inline const static int SCSI_DELAY_SEND_DATA_DAYNAPORT_NS = 100'000; +}; diff --git a/cpp/buses/gpiobus_raspberry.cpp b/cpp/buses/gpiobus_raspberry.cpp new file mode 100644 index 00000000..3ec1671c --- /dev/null +++ b/cpp/buses/gpiobus_raspberry.cpp @@ -0,0 +1,869 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Powered by XM6 TypeG Technology. +// Copyright (C) 2016-2020 GIMONS +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "buses/gpiobus_raspberry.h" +#include +#include +#include +#ifdef USE_SEL_EVENT_ENABLE +#include +#endif +#include +#include +#include + +//--------------------------------------------------------------------------- +// +// imported from bcm_host.c +// +//--------------------------------------------------------------------------- +uint32_t GPIOBUS_Raspberry::get_dt_ranges(const char *filename, uint32_t offset) +{ + uint32_t address = ~0; + if (FILE *fp = fopen(filename, "rb"); fp) { + fseek(fp, offset, SEEK_SET); + if (array buf; fread(buf.data(), 1, buf.size(), fp) == buf.size()) { + address = (int)buf[0] << 24 | (int)buf[1] << 16 | (int)buf[2] << 8 | (int)buf[3] << 0; + } + fclose(fp); + } + return address; +} + +uint32_t GPIOBUS_Raspberry::bcm_host_get_peripheral_address() +{ +#ifdef __linux__ + uint32_t address = get_dt_ranges("/proc/device-tree/soc/ranges", 4); + if (!address) { + address = get_dt_ranges("/proc/device-tree/soc/ranges", 8); + } + address = (address == (uint32_t) ~ 0) ? 0x20000000 : address; + return address; +#else + return 0; +#endif +} + +bool GPIOBUS_Raspberry::Init(mode_e mode) +{ + GPIOBUS::Init(mode); + + // Get the base address + baseaddr = bcm_host_get_peripheral_address(); + + int fd = open("/dev/mem", O_RDWR | O_SYNC); + if (fd == -1) { + spdlog::error("Error: Unable to open /dev/mem"); + return false; + } + + // Map peripheral region memory + void *map = mmap(nullptr, 0x1000100, PROT_READ | PROT_WRITE, MAP_SHARED, fd, baseaddr); + if (map == MAP_FAILED) { + spdlog::error("Error: Unable to map memory: "+ string(strerror(errno))); + close(fd); + return false; + } + + // Determine the type of raspberry pi from the base address + if (baseaddr == 0xfe000000) { + rpitype = 4; + } else if (baseaddr == 0x3f000000) { + rpitype = 2; + } else { + rpitype = 1; + } + + // GPIO + gpio = static_cast(map); + gpio += GPIO_OFFSET / sizeof(uint32_t); + level = &gpio[GPIO_LEV_0]; + + // PADS + pads = static_cast(map); + pads += PADS_OFFSET / sizeof(uint32_t); + + // Interrupt controller + irpctl = static_cast(map); + irpctl += IRPT_OFFSET / sizeof(uint32_t); + + // Quad-A7 control + qa7regs = static_cast(map); + qa7regs += QA7_OFFSET / sizeof(uint32_t); + + // Map GIC memory + if (rpitype == 4) { + map = mmap(nullptr, 8192, PROT_READ | PROT_WRITE, MAP_SHARED, fd, ARM_GICD_BASE); + if (map == MAP_FAILED) { + close(fd); + return false; + } + gicd = static_cast(map); + gicc = static_cast(map); + gicc += (ARM_GICC_BASE - ARM_GICD_BASE) / sizeof(uint32_t); + } else { + gicd = nullptr; + gicc = nullptr; + } + close(fd); + + // Set Drive Strength to 16mA + SetSignalDriveStrength(7); + + // Set pull up/pull down +#if SIGNAL_CONTROL_MODE == 0 + int pullmode = GPIO_PULLNONE; +#elif SIGNAL_CONTROL_MODE == 1 + int pullmode = GPIO_PULLUP; +#else + int pullmode = GPIO_PULLDOWN; +#endif + + // Initialize all signals + for (int i = 0; SignalTable[i] >= 0; i++) { + int j = SignalTable[i]; + PinSetSignal(j, false); + PinConfig(j, GPIO_INPUT); + PullConfig(j, pullmode); + } + + // Set control signals + PinSetSignal(PIN_ACT, false); + PinSetSignal(PIN_TAD, false); + PinSetSignal(PIN_IND, false); + PinSetSignal(PIN_DTD, false); + PinConfig(PIN_ACT, GPIO_OUTPUT); + PinConfig(PIN_TAD, GPIO_OUTPUT); + PinConfig(PIN_IND, GPIO_OUTPUT); + PinConfig(PIN_DTD, GPIO_OUTPUT); + + // Set the ENABLE signal + // This is used to show that the application is running + PinSetSignal(PIN_ENB, ENB_OFF); + PinConfig(PIN_ENB, GPIO_OUTPUT); + + // GPIO Function Select (GPFSEL) registers backup + gpfsel[0] = gpio[GPIO_FSEL_0]; + gpfsel[1] = gpio[GPIO_FSEL_1]; + gpfsel[2] = gpio[GPIO_FSEL_2]; + gpfsel[3] = gpio[GPIO_FSEL_3]; + + // Initialize SEL signal interrupt +#ifdef USE_SEL_EVENT_ENABLE + fd = open("/dev/gpiochip0", 0); + if (fd == -1) { + spdlog::error("Unable to open /dev/gpiochip0. If s2p is running, please shut it down first."); + return false; + } + + // Event request setting + strcpy(selevreq.consumer_label, "SCSI2Pi"); //NOSONAR Using strcpy is safe + selevreq.lineoffset = PIN_SEL; + selevreq.handleflags = GPIOHANDLE_REQUEST_INPUT; +#if SIGNAL_CONTROL_MODE < 2 + selevreq.eventflags = GPIOEVENT_REQUEST_FALLING_EDGE; +#else + selevreq.eventflags = GPIOEVENT_REQUEST_RISING_EDGE; +#endif + + if (ioctl(fd, GPIO_GET_LINEEVENT_IOCTL, &selevreq) == -1) { + spdlog::error("Unable to register event request. If s2p is running, please shut it down first."); + close(fd); + return false; + } + + // Close GPIO chip file handle + close(fd); + + // epoll initialization + epfd = epoll_create(1); + epoll_event ev = { }; + ev.events = EPOLLIN | EPOLLPRI; + ev.data.fd = selevreq.fd; + epoll_ctl(epfd, EPOLL_CTL_ADD, selevreq.fd, &ev); +#else + // Edge detection setting +#if SIGNAL_CONTROL_MODE == 2 + gpio[GPIO_AREN_0] = 1 << PIN_SEL; +#else + gpio[GPIO_AFEN_0] = 1 << PIN_SEL; +#endif + + // Clear event - GPIO Pin Event Detect Status + gpio[GPIO_EDS_0] = 1 << PIN_SEL; + +#ifdef __linux__ + // Register interrupt handler + setIrqFuncAddress(IrqHandler); +#endif + + // GPIO interrupt setting + if (rpitype == 4) { + // GIC Invalid + gicd[GICD_CTLR] = 0; + + // Route all interupts to core 0 + for (int i = 0; i < 8; i++) { + gicd[GICD_ICENABLER0 + i] = 0xffffffff; + gicd[GICD_ICPENDR0 + i] = 0xffffffff; + gicd[GICD_ICACTIVER0 + i] = 0xffffffff; + } + for (int i = 0; i < 64; i++) { + gicd[GICD_IPRIORITYR0 + i] = 0xa0a0a0a0; + gicd[GICD_ITARGETSR0 + i] = 0x01010101; + } + + // Set all interrupts as level triggers + for (int i = 0; i < 16; i++) { + gicd[GICD_ICFGR0 + i] = 0; + } + + // GIC Invalid + gicd[GICD_CTLR] = 1; + + // Enable CPU interface for core 0 + gicc[GICC_PMR] = 0xf0; + gicc[GICC_CTLR] = 1; + + // Enable interrupts + gicd[GICD_ISENABLER0 + (GIC_GPIO_IRQ / 32)] = 1 << (GIC_GPIO_IRQ % 32); + } else { + // Enable interrupts + irpctl[IRPT_ENB_IRQ_2] = (1 << (GPIO_IRQ % 32)); + } +#endif + + // Create work table + MakeTable(); + + // Finally, enable ENABLE + // Show the user that this app is running + SetControl(PIN_ENB, ENB_ON); + + return true; +} + +void GPIOBUS_Raspberry::CleanUp() +{ + // Release SEL signal interrupt +#ifdef USE_SEL_EVENT_ENABLE + close(selevreq.fd); +#endif + + // Set control signals + PinSetSignal(PIN_ENB, false); + PinSetSignal(PIN_ACT, false); + PinSetSignal(PIN_TAD, false); + PinSetSignal(PIN_IND, false); + PinSetSignal(PIN_DTD, false); + PinConfig(PIN_ACT, GPIO_INPUT); + PinConfig(PIN_TAD, GPIO_INPUT); + PinConfig(PIN_IND, GPIO_INPUT); + PinConfig(PIN_DTD, GPIO_INPUT); + + // Initialize all signals + for (int i = 0; SignalTable[i] >= 0; i++) { + const int pin = SignalTable[i]; + PinSetSignal(pin, OFF); + PinConfig(pin, GPIO_INPUT); + PullConfig(pin, GPIO_PULLNONE); + } + + // Set drive strength back to 8mA + SetSignalDriveStrength(3); +} + +void GPIOBUS_Raspberry::Reset() +{ + // Turn off active signal + SetControl(PIN_ACT, false); + + // Set all signals to off + for (int i = 0;; i++) { + const int signal = SignalTable[i]; + if (signal < 0) { + break; + } + + SetSignal(signal, false); + } + + // Set target signal to input for all modes + SetControl(PIN_TAD, TAD_IN); + SetMode(PIN_BSY, IN); + SetMode(PIN_MSG, IN); + SetMode(PIN_CD, IN); + SetMode(PIN_REQ, IN); + SetMode(PIN_IO, IN); + + if (operation_mode == mode_e::TARGET) { + // Set the initiator signal to input + SetControl(PIN_IND, IND_IN); + SetMode(PIN_SEL, IN); + SetMode(PIN_ATN, IN); + SetMode(PIN_ACK, IN); + SetMode(PIN_RST, IN); + + // Set data bus signals to input + SetControl(PIN_DTD, DTD_IN); + SetMode(PIN_DT0, IN); + SetMode(PIN_DT1, IN); + SetMode(PIN_DT2, IN); + SetMode(PIN_DT3, IN); + SetMode(PIN_DT4, IN); + SetMode(PIN_DT5, IN); + SetMode(PIN_DT6, IN); + SetMode(PIN_DT7, IN); + } else { + // Set the initiator signal to output + SetControl(PIN_IND, IND_OUT); + SetMode(PIN_SEL, OUT); + SetMode(PIN_ATN, OUT); + SetMode(PIN_ACK, OUT); + SetMode(PIN_RST, OUT); + + // Set the data bus signals to output + SetControl(PIN_DTD, DTD_OUT); + SetMode(PIN_DT0, OUT); + SetMode(PIN_DT1, OUT); + SetMode(PIN_DT2, OUT); + SetMode(PIN_DT3, OUT); + SetMode(PIN_DT4, OUT); + SetMode(PIN_DT5, OUT); + SetMode(PIN_DT6, OUT); + SetMode(PIN_DT7, OUT); + } + + // Initialize all signals + signals = 0; +} + +bool GPIOBUS_Raspberry::GetBSY() const +{ + return GetSignal(PIN_BSY); +} + +void GPIOBUS_Raspberry::SetBSY(bool ast) +{ + // Set BSY signal + SetSignal(PIN_BSY, ast); + + if (ast) { + SetControl(PIN_ACT, true); + + SetControl(PIN_TAD, TAD_OUT); + + SetMode(PIN_BSY, OUT); + SetMode(PIN_MSG, OUT); + SetMode(PIN_CD, OUT); + SetMode(PIN_REQ, OUT); + SetMode(PIN_IO, OUT); + } else { + SetControl(PIN_ACT, false); + + SetControl(PIN_TAD, TAD_IN); + + SetMode(PIN_BSY, IN); + SetMode(PIN_MSG, IN); + SetMode(PIN_CD, IN); + SetMode(PIN_REQ, IN); + SetMode(PIN_IO, IN); + } +} + +bool GPIOBUS_Raspberry::GetSEL() const +{ + return GetSignal(PIN_SEL); +} + +void GPIOBUS_Raspberry::SetSEL(bool ast) +{ + if (operation_mode == mode_e::INITIATOR && ast) { + SetControl(PIN_ACT, true); + } + + SetSignal(PIN_SEL, ast); +} + +bool GPIOBUS_Raspberry::GetATN() const +{ + return GetSignal(PIN_ATN); +} + +void GPIOBUS_Raspberry::SetATN(bool ast) +{ + SetSignal(PIN_ATN, ast); +} + +bool GPIOBUS_Raspberry::GetACK() const +{ + return GetSignal(PIN_ACK); +} + +void GPIOBUS_Raspberry::SetACK(bool ast) +{ + SetSignal(PIN_ACK, ast); +} + +bool GPIOBUS_Raspberry::GetRST() const +{ + return GetSignal(PIN_RST); +} + +void GPIOBUS_Raspberry::SetRST(bool ast) +{ + SetSignal(PIN_RST, ast); +} + +bool GPIOBUS_Raspberry::GetMSG() const +{ + return GetSignal(PIN_MSG); +} + +void GPIOBUS_Raspberry::SetMSG(bool ast) +{ + SetSignal(PIN_MSG, ast); +} + +bool GPIOBUS_Raspberry::GetCD() const +{ + return GetSignal(PIN_CD); +} + +void GPIOBUS_Raspberry::SetCD(bool ast) +{ + SetSignal(PIN_CD, ast); +} + +bool GPIOBUS_Raspberry::GetIO() +{ + bool ast = GetSignal(PIN_IO); + + if (operation_mode == mode_e::INITIATOR) { + // Change the data input/output direction by IO signal + if (ast) { + SetControl(PIN_DTD, DTD_IN); + SetMode(PIN_DT0, IN); + SetMode(PIN_DT1, IN); + SetMode(PIN_DT2, IN); + SetMode(PIN_DT3, IN); + SetMode(PIN_DT4, IN); + SetMode(PIN_DT5, IN); + SetMode(PIN_DT6, IN); + SetMode(PIN_DT7, IN); + } else { + SetControl(PIN_DTD, DTD_OUT); + SetMode(PIN_DT0, OUT); + SetMode(PIN_DT1, OUT); + SetMode(PIN_DT2, OUT); + SetMode(PIN_DT3, OUT); + SetMode(PIN_DT4, OUT); + SetMode(PIN_DT5, OUT); + SetMode(PIN_DT6, OUT); + SetMode(PIN_DT7, OUT); + } + } + + return ast; +} + +void GPIOBUS_Raspberry::SetIO(bool ast) +{ + assert(operation_mode == mode_e::TARGET); + + SetSignal(PIN_IO, ast); + + // Change the data input/output direction by IO signal + if (ast) { + SetControl(PIN_DTD, DTD_OUT); + SetDAT(0); + SetMode(PIN_DT0, OUT); + SetMode(PIN_DT1, OUT); + SetMode(PIN_DT2, OUT); + SetMode(PIN_DT3, OUT); + SetMode(PIN_DT4, OUT); + SetMode(PIN_DT5, OUT); + SetMode(PIN_DT6, OUT); + SetMode(PIN_DT7, OUT); + } else { + SetControl(PIN_DTD, DTD_IN); + SetMode(PIN_DT0, IN); + SetMode(PIN_DT1, IN); + SetMode(PIN_DT2, IN); + SetMode(PIN_DT3, IN); + SetMode(PIN_DT4, IN); + SetMode(PIN_DT5, IN); + SetMode(PIN_DT6, IN); + SetMode(PIN_DT7, IN); + } +} + +bool GPIOBUS_Raspberry::GetREQ() const +{ + return GetSignal(PIN_REQ); +} + +void GPIOBUS_Raspberry::SetREQ(bool ast) +{ + SetSignal(PIN_REQ, ast); +} + +uint8_t GPIOBUS_Raspberry::GetDAT() +{ + return static_cast(Acquire() >> PIN_DT0); +} + +void GPIOBUS_Raspberry::SetDAT(uint8_t dat) +{ + // Write to port +#if SIGNAL_CONTROL_MODE == 0 + uint32_t fsel = gpfsel[0]; + fsel &= tblDatMsk[0][dat]; + fsel |= tblDatSet[0][dat]; + gpfsel[0] = fsel; + gpio[GPIO_FSEL_0] = fsel; + + fsel = gpfsel[1]; + fsel &= tblDatMsk[1][dat]; + fsel |= tblDatSet[1][dat]; + gpfsel[1] = fsel; + gpio[GPIO_FSEL_1] = fsel; + + fsel = gpfsel[2]; + fsel &= tblDatMsk[2][dat]; + fsel |= tblDatSet[2][dat]; + gpfsel[2] = fsel; + gpio[GPIO_FSEL_2] = fsel; +#else + gpio[GPIO_CLR_0] = tblDatMsk[dat]; + gpio[GPIO_SET_0] = tblDatSet[dat]; +#endif +} + +//--------------------------------------------------------------------------- +// +// Signal table +// +//--------------------------------------------------------------------------- +const array GPIOBUS_Raspberry::SignalTable = {PIN_DT0, PIN_DT1, PIN_DT2, PIN_DT3, PIN_DT4, PIN_DT5, PIN_DT6, + PIN_DT7, PIN_DP, PIN_SEL, PIN_ATN, PIN_RST, PIN_ACK, PIN_BSY, + PIN_MSG, PIN_CD, PIN_IO, PIN_REQ, -1}; + +//--------------------------------------------------------------------------- +// +// Create work table +// +//--------------------------------------------------------------------------- +void GPIOBUS_Raspberry::MakeTable(void) +{ + const array pintbl = {PIN_DT0, PIN_DT1, PIN_DT2, PIN_DT3, PIN_DT4, PIN_DT5, PIN_DT6, PIN_DT7, PIN_DP}; + + array tblParity; + + // Create parity table + for (uint32_t i = 0; i < 0x100; i++) { + uint32_t bits = i; + uint32_t parity = 0; + for (int j = 0; j < 8; j++) { + parity ^= bits & 1; + bits >>= 1; + } + parity = ~parity; + tblParity[i] = parity & 1; + } + +#if SIGNAL_CONTROL_MODE == 0 + // Mask and setting data generation + for (auto &tbl : tblDatMsk) { + tbl.fill(-1); + } + for (auto &tbl : tblDatSet) { + tbl.fill(0); + } + + for (uint32_t i = 0; i < 0x100; i++) { + // Bit string for inspection + uint32_t bits = i; + + // Get parity + if (tblParity[i]) { + bits |= (1 << 8); + } + + // Bit check + for (int j = 0; j < 9; j++) { + // Index and shift amount calculation + int index = pintbl[j] / 10; + int shift = (pintbl[j] % 10) * 3; + + // Mask data + tblDatMsk[index][i] &= ~(0x7 << shift); + + // Setting data + if (bits & 1) { + tblDatSet[index][i] |= (1 << shift); + } + + bits >>= 1; + } + } +#else + for (uint32_t i = 0; i < 0x100; i++) { + // Bit string for inspection + uint32_t bits = i; + + // Get parity + if (tblParity[i]) { + bits |= (1 << 8); + } + +#if SIGNAL_CONTROL_MODE == 1 + // Negative logic is inverted + bits = ~bits; +#endif + + // Create GPIO register information + uint32_t gpclr = 0; + uint32_t gpset = 0; + for (int j = 0; j < 9; j++) { + if (bits & 1) { + gpset |= (1 << pintbl[j]); + } else { + gpclr |= (1 << pintbl[j]); + } + bits >>= 1; + } + + tblDatMsk[i] = gpclr; + tblDatSet[i] = gpset; + } +#endif +} + +void GPIOBUS_Raspberry::SetControl(int pin, bool ast) +{ + PinSetSignal(pin, ast); +} + +//--------------------------------------------------------------------------- +// +// Input/output mode setting +// +// Set direction fo pin (IN / OUT) +// Used with: TAD, BSY, MSG, CD, REQ, O, SEL, IND, ATN, ACK, RST, DT* +// +//--------------------------------------------------------------------------- +void GPIOBUS_Raspberry::SetMode(int pin, int mode) +{ +#if SIGNAL_CONTROL_MODE == 0 + if (mode == OUT) { + return; + } +#endif + + const int index = pin / 10; + const int shift = (pin % 10) * 3; + uint32_t data = gpfsel[index]; + data &= ~(0x7 << shift); + if (mode == OUT) { + data |= (1 << shift); + } + gpio[index] = data; + gpfsel[index] = data; +} + +//--------------------------------------------------------------------------- +// +// Get input signal value +// +//--------------------------------------------------------------------------- +bool GPIOBUS_Raspberry::GetSignal(int pin) const +{ + return (signals >> pin) & 1; +} + +//--------------------------------------------------------------------------- +// +// Set output signal value +// +// Sets the output value. Used with: +// PIN_ENB, ACT, TAD, IND, DTD, BSY, SignalTable +// +//--------------------------------------------------------------------------- +void GPIOBUS_Raspberry::SetSignal(int pin, bool ast) +{ +#if SIGNAL_CONTROL_MODE == 0 + const int index = pin / 10; + const int shift = (pin % 10) * 3; + uint32_t data = gpfsel[index]; + if (ast) { + data |= (1 << shift); + } else { + data &= ~(0x7 << shift); + } + gpio[index] = data; + gpfsel[index] = data; +#elif SIGNAL_CONTROL_MODE == 1 + if (ast) { + gpio[GPIO_CLR_0] = 0x1 << pin; + } else { + gpio[GPIO_SET_0] = 0x1 << pin; + } +#elif SIGNAL_CONTROL_MODE == 2 + if (ast) { + gpio[GPIO_SET_0] = 0x1 << pin; + } else { + gpio[GPIO_CLR_0] = 0x1 << pin; + } +#endif +} + +// TODO This could be faster. Use a function pointer based on the Pi type, which you determine only once +void GPIOBUS_Raspberry::DisableIRQ() +{ +#ifndef NO_IRQ_DISABLE +#ifdef __linux__ + if (rpitype == 4) { + // RPI4 is disabled by GICC + giccpmr = gicc[GICC_PMR]; + gicc[GICC_PMR] = 0; + } else if (rpitype == 2) { + // RPI2,3 disable core timer IRQ + tintcore = sched_getcpu() + QA7_CORE0_TINTC; + tintctl = qa7regs[tintcore]; + qa7regs[tintcore] = 0; + } else { + // Stop system timer interrupt with interrupt controller + irptenb = irpctl[IRPT_ENB_IRQ_1]; + irpctl[IRPT_DIS_IRQ_1] = irptenb & 0xf; + } +#endif +#endif + } + +void GPIOBUS_Raspberry::EnableIRQ() +{ +#ifndef NO_IRQ_DISABLE + if (rpitype == 4) { + // RPI4 enables interrupts via the GICC + gicc[GICC_PMR] = giccpmr; + } else if (rpitype == 2) { + // RPI2,3 re-enable core timer IRQ + qa7regs[tintcore] = tintctl; + } else { + // Restart the system timer interrupt with the interrupt controller + irpctl[IRPT_ENB_IRQ_1] = irptenb & 0xf; + } +#endif +} + +//--------------------------------------------------------------------------- +// +// Pin direction setting (input/output) +// +// Used in Init() for ACT, TAD, IND, DTD, ENB to set direction (GPIO_OUTPUT vs GPIO_INPUT) +// Also used on SignalTable +// Only used in Init and Cleanup. Reset uses SetMode +//--------------------------------------------------------------------------- +void GPIOBUS_Raspberry::PinConfig(int pin, int mode) +{ + // Check for invalid pin + if (pin < 0) { + return; + } + + int index = pin / 10; + uint32_t mask = ~(0x7 << ((pin % 10) * 3)); + gpio[index] = (gpio[index] & mask) | ((mode & 0x7) << ((pin % 10) * 3)); +} + +//--------------------------------------------------------------------------- +// +// Pin pull-up/pull-down setting +// +//--------------------------------------------------------------------------- +void GPIOBUS_Raspberry::PullConfig(int pin, int mode) +{ + // Check for invalid pin + if (pin < 0) { + return; + } + + if (rpitype == 4) { + uint32_t pull; + switch (mode) { + case GPIO_PULLNONE: + pull = 0; + break; + case GPIO_PULLUP: + pull = 1; + break; + case GPIO_PULLDOWN: + pull = 2; + break; + default: + return; + } + + pin &= 0x1f; + int shift = (pin & 0xf) << 1; + uint32_t bits = gpio[GPIO_PUPPDN0 + (pin >> 4)]; + bits &= ~(3 << shift); + bits |= (pull << shift); + gpio[GPIO_PUPPDN0 + (pin >> 4)] = bits; + } else { + // 2 us + const timespec ts = {.tv_sec = 0, .tv_nsec = 2'000}; + + pin &= 0x1f; + gpio[GPIO_PUD] = mode & 0x3; + nanosleep(&ts, nullptr); + gpio[GPIO_CLK_0] = 0x1 << pin; + nanosleep(&ts, nullptr); + gpio[GPIO_PUD] = 0; + gpio[GPIO_CLK_0] = 0; + } +} + +//--------------------------------------------------------------------------- +// +// Set output pin +// +//--------------------------------------------------------------------------- +void GPIOBUS_Raspberry::PinSetSignal(int pin, bool ast) +{ + // Check for invalid pin + if (pin >= 0) { + gpio[ast ? GPIO_SET_0 : GPIO_CLR_0] = 1 << pin; + } +} + +void GPIOBUS_Raspberry::SetSignalDriveStrength(uint32_t drive) +{ + const uint32_t data = pads[PAD_0_27]; + pads[PAD_0_27] = (0xFFFFFFF8 & data) | drive | 0x5a000000; +} + +//--------------------------------------------------------------------------- +// +// Bus signal acquisition +// +//--------------------------------------------------------------------------- +uint32_t GPIOBUS_Raspberry::Acquire() +{ + signals = *level; + +#if SIGNAL_CONTROL_MODE < 2 + // Invert if negative logic (internal processing is unified to positive logic) + signals = ~signals; +#endif + + return signals; +} diff --git a/cpp/buses/gpiobus_raspberry.h b/cpp/buses/gpiobus_raspberry.h new file mode 100644 index 00000000..ae4ab4f9 --- /dev/null +++ b/cpp/buses/gpiobus_raspberry.h @@ -0,0 +1,214 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Powered by XM6 TypeG Technology. +// Copyright (C) 2016-2020 GIMONS +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "buses/gpiobus.h" + +// Constant declarations (GIC) +const static uint32_t ARM_GICD_BASE = 0xFF841000; +const static uint32_t ARM_GICC_BASE = 0xFF842000; +const static uint32_t ARM_GIC_END = 0xFF847FFF; +const static int GICD_CTLR = 0x000; +const static int GICD_IGROUPR0 = 0x020; +const static int GICD_ISENABLER0 = 0x040; +const static int GICD_ICENABLER0 = 0x060; +const static int GICD_ISPENDR0 = 0x080; +const static int GICD_ICPENDR0 = 0x0A0; +const static int GICD_ISACTIVER0 = 0x0C0; +const static int GICD_ICACTIVER0 = 0x0E0; +const static int GICD_IPRIORITYR0 = 0x100; +const static int GICD_ITARGETSR0 = 0x200; +const static int GICD_ICFGR0 = 0x300; +const static int GICD_SGIR = 0x3C0; +const static int GICC_CTLR = 0x000; +const static int GICC_PMR = 0x001; +const static int GICC_IAR = 0x003; +const static int GICC_EOIR = 0x004; + +// Constant declarations (GIC IRQ) +const static int GIC_IRQLOCAL0 = (16 + 14); +// GPIO3 +const static int GIC_GPIO_IRQ = (32 + 116); + +class GPIOBUS_Raspberry : public GPIOBUS +{ + +public: + + GPIOBUS_Raspberry() = default; + ~GPIOBUS_Raspberry() override = default; + + bool Init(mode_e mode = mode_e::TARGET) override; + + void Reset() override; + void CleanUp() override; + + // Bus signal acquisition + uint32_t Acquire() override; + + bool GetBSY() const override; + void SetBSY(bool) override; + + bool GetSEL() const override; + void SetSEL(bool) override; + + bool GetATN() const override; + void SetATN(bool) override; + + bool GetACK() const override; + void SetACK(bool) override; + + bool GetRST() const override; + void SetRST(bool) override; + + bool GetMSG() const override; + void SetMSG(bool) override; + + bool GetCD() const override; + void SetCD(bool) override; + + bool GetIO() override; + void SetIO(bool ast) override; + bool GetREQ() const override; + void SetREQ(bool ast) override; + + uint8_t GetDAT() override; + void SetDAT(uint8_t) override; + + bool WaitREQ(bool ast) override + { + return WaitSignal(PIN_REQ, ast); + } + bool WaitACK(bool ast) override + { + return WaitSignal(PIN_ACK, ast); + } + static uint32_t bcm_host_get_peripheral_address(); + +protected: + + // All bus signals + uint32_t signals = 0; + + // GPIO input level + volatile uint32_t *level = nullptr; + +private: + + void MakeTable() override; + + void SetControl(int, bool) override; + void SetMode(int, int) override; + + bool GetSignal(int) const override; + void SetSignal(int, bool) override; + + void DisableIRQ() override; + void EnableIRQ() override; + + void PinConfig(int, int) override; + void PullConfig(int, int) override; + void PinSetSignal(int, bool) override; + void SetSignalDriveStrength(uint32_t) override; + + static uint32_t get_dt_ranges(const char *, uint32_t); + + uint32_t baseaddr = 0; + + int rpitype = 0; + + // GPIO register + volatile uint32_t *gpio = nullptr; // NOSONAR: volatile needed for register access + + // PADS register + volatile uint32_t *pads = nullptr; // NOSONAR: volatile needed for register access + + // Interrupt control register + volatile uint32_t *irpctl = nullptr; + + // Interrupt enabled state + volatile uint32_t irptenb; // NOSONAR: volatile needed for register access + + // QA7 register + volatile uint32_t *qa7regs = nullptr; + + // Interupt control target CPU. + volatile int tintcore; // NOSONAR: volatile needed for register access + + // Interupt control + volatile uint32_t tintctl; // NOSONAR: volatile needed for register access + + // GICC priority setting + volatile uint32_t giccpmr; // NOSONAR: volatile needed for register access + + // GIC Interrupt distributor register + volatile uint32_t *gicd = nullptr; + + // GIC CPU interface register + volatile uint32_t *gicc = nullptr; + + // RAM copy of GPFSEL0-4 values (GPIO Function Select) + array gpfsel; + +#if SIGNAL_CONTROL_MODE == 0 + // Data mask table + array, 3> tblDatMsk; + // Data setting table + array, 3> tblDatSet; +#else + // Data mask table + array tblDatMsk = {}; + // Table setting table + array tblDatSet = {}; +#endif + + static const array SignalTable; + + const static int GPIO_FSEL_0 = 0; + const static int GPIO_FSEL_1 = 1; + const static int GPIO_FSEL_2 = 2; + const static int GPIO_FSEL_3 = 3; + const static int GPIO_SET_0 = 7; + const static int GPIO_CLR_0 = 10; + const static int GPIO_LEV_0 = 13; + const static int GPIO_EDS_0 = 16; + const static int GPIO_REN_0 = 19; + const static int GPIO_FEN_0 = 22; + const static int GPIO_HEN_0 = 25; + const static int GPIO_LEN_0 = 28; + const static int GPIO_AREN_0 = 31; + const static int GPIO_AFEN_0 = 34; + const static int GPIO_PUD = 37; + const static int GPIO_CLK_0 = 38; + const static int GPIO_GPPINMUXSD = 52; + const static int GPIO_PUPPDN0 = 57; + const static int GPIO_PUPPDN1 = 58; + const static int GPIO_PUPPDN3 = 59; + const static int GPIO_PUPPDN4 = 60; + const static int PAD_0_27 = 11; + const static int IRPT_PND_IRQ_B = 0; + const static int IRPT_PND_IRQ_1 = 1; + const static int IRPT_PND_IRQ_2 = 2; + const static int IRPT_FIQ_CNTL = 3; + const static int IRPT_ENB_IRQ_1 = 4; + const static int IRPT_ENB_IRQ_2 = 5; + const static int IRPT_ENB_IRQ_B = 6; + const static int IRPT_DIS_IRQ_1 = 7; + const static int IRPT_DIS_IRQ_2 = 8; + const static int IRPT_DIS_IRQ_B = 9; + const static int QA7_CORE0_TINTC = 16; + const static int GPIO_IRQ = (32 + 20); // GPIO3 + + const static uint32_t IRPT_OFFSET = 0x0000B200; + const static uint32_t PADS_OFFSET = 0x00100000; + const static uint32_t GPIO_OFFSET = 0x00200000; + const static uint32_t QA7_OFFSET = 0x01000000; +}; diff --git a/cpp/buses/in_process_bus.cpp b/cpp/buses/in_process_bus.cpp new file mode 100644 index 00000000..ecfca17f --- /dev/null +++ b/cpp/buses/in_process_bus.cpp @@ -0,0 +1,96 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "buses/in_process_bus.h" +#include + +void InProcessBus::Reset() +{ + signals = { }; + + dat = 0; +} + +bool InProcessBus::GetSignal(int pin) const +{ + return signals[pin]; +} + +void InProcessBus::SetSignal(int pin, bool state) +{ + scoped_lock lock(write_locker); + signals[pin] = state; +} + +bool InProcessBus::WaitSignal(int pin, bool state) +{ + const auto now = chrono::steady_clock::now(); + + do { + if (signals[pin] == state) { + return true; + } + + if (signals[PIN_RST]) { + return false; + } + } while ((chrono::duration_cast(chrono::steady_clock::now() - now).count()) < 3); + + return false; +} + +bool InProcessBus::WaitForSelectEvent() +{ + // TODO Is there a better way to avoid busy waiting? + const timespec ts = { .tv_sec = 0, .tv_nsec = 10'000'000 }; + nanosleep(&ts, nullptr); + + return true; +} + +void DelegatingInProcessBus::Reset() +{ + spdlog::trace(GetMode() + ": Resetting bus"); + + bus.Reset(); +} + +bool DelegatingInProcessBus::GetSignal(int pin) const +{ + const bool state = bus.GetSignal(pin); + + if (log_signals && pin != PIN_ACK && pin != PIN_REQ && spdlog::get_level() == spdlog::level::trace) { + spdlog::trace(GetMode() + ": Getting " + GetSignalName(pin) + (state ? ": true" : ": false")); + } + + return state; +} + +void DelegatingInProcessBus::SetSignal(int pin, bool state) +{ + if (log_signals && pin != PIN_ACK && pin != PIN_REQ && spdlog::get_level() == spdlog::level::trace) { + spdlog::trace(GetMode() + ": Setting " + GetSignalName(pin) + " to " + (state ? "true" : "false")); + } + + bus.SetSignal(pin, state); +} + +bool DelegatingInProcessBus::WaitSignal(int pin, bool state) +{ + if (log_signals && pin != PIN_ACK && pin != PIN_REQ && spdlog::get_level() == spdlog::level::trace) { + spdlog::trace(GetMode() + ": Waiting for " + GetSignalName(pin) + " to become " + (state ? "true" : "false")); + } + + return bus.WaitSignal(pin, state); +} + +string DelegatingInProcessBus::GetSignalName(int pin) const +{ + const auto &it = SIGNALS.find(pin); + return it != SIGNALS.end() ? it->second : "????"; +} diff --git a/cpp/buses/in_process_bus.h b/cpp/buses/in_process_bus.h new file mode 100644 index 00000000..a9ea4c0e --- /dev/null +++ b/cpp/buses/in_process_bus.h @@ -0,0 +1,256 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "buses/gpiobus.h" +#include +#include +#include +#include +#include + +class InProcessBus: public GPIOBUS +{ + +public: + + InProcessBus() = default; + ~InProcessBus() override = default; + + void Reset() override; + + void CleanUp() override + { + // Nothing to do + } + + uint32_t Acquire() override + { + return dat; + } + + bool GetBSY() const override + { + return GetSignal(PIN_BSY); + } + void SetBSY(bool state) override + { + SetSignal(PIN_BSY, state); + } + bool GetSEL() const override + { + return GetSignal(PIN_SEL); + } + void SetSEL(bool state) override + { + SetSignal(PIN_SEL, state); + } + bool GetATN() const override + { + return GetSignal(PIN_ATN); + } + void SetATN(bool state) override + { + SetSignal(PIN_ATN, state); + } + bool GetACK() const override + { + return GetSignal(PIN_ACK); + } + void SetACK(bool state) override + { + SetSignal(PIN_ACK, state); + } + bool GetRST() const override + { + return GetSignal(PIN_RST); + } + void SetRST(bool state) override + { + SetSignal(PIN_RST, state); + } + ; + bool GetMSG() const override + { + return GetSignal(PIN_MSG); + } + ; + void SetMSG(bool state) override + { + SetSignal(PIN_MSG, state); + } + ; + bool GetCD() const override + { + return GetSignal(PIN_CD); + } + void SetCD(bool state) override + { + SetSignal(PIN_CD, state); + } + bool GetIO() override + { + return GetSignal(PIN_IO); + } + void SetIO(bool state) override + { + SetSignal(PIN_IO, state); + } + bool GetREQ() const override + { + return GetSignal(PIN_REQ); + } + void SetREQ(bool state) override + { + SetSignal(PIN_REQ, state); + } + + bool WaitSignal(int, bool) override; + + bool WaitREQ(bool state) override + { + return WaitSignal(PIN_REQ, state); + } + + bool WaitACK(bool state) override + { + return WaitSignal(PIN_ACK, state); + } + + uint8_t GetDAT() override + { + return dat; + } + void SetDAT(uint8_t d) override + { + dat = d; + } + + bool GetSignal(int pin) const override; + void SetSignal(int, bool) override; + + bool WaitForSelectEvent() override; + +private: + + void DisableIRQ() override + { + // Nothing to do + } + void EnableIRQ() override + { + // Nothing to do } + } + + void MakeTable() override + { + assert(false); + } + void SetControl(int, bool) override + { + assert(false); + } + void SetMode(int, int) override + { + assert(false); + } + void PinConfig(int, int) override + { + assert(false); + } + void PullConfig(int, int) override + { + assert(false); + } + void PinSetSignal(int, bool) override + { + assert(false); + } + void SetSignalDriveStrength(uint32_t) override + { + assert(false); + } + + mutex write_locker; + + atomic dat = 0; + + array signals = { }; +}; + +class DelegatingInProcessBus: public InProcessBus +{ + +public: + + DelegatingInProcessBus(InProcessBus &b, bool l) : bus(b), log_signals(l) + { + } + ~DelegatingInProcessBus() override = default; + + void Reset() override; + + void CleanUp() override + { + bus.CleanUp(); + } + + uint32_t Acquire() override + { + return bus.Acquire(); + } + + bool WaitREQ(bool state) override + { + return bus.WaitSignal(PIN_REQ, state); + } + + bool WaitACK(bool state) override + { + return bus.WaitSignal(PIN_ACK, state); + } + + uint8_t GetDAT() override + { + return bus.GetDAT(); + } + void SetDAT(uint8_t dat) override + { + bus.SetDAT(dat); + } + + bool GetSignal(int) const override; + void SetSignal(int, bool) override; + bool WaitSignal(int, bool) override; + +private: + + string GetMode() const + { + return operation_mode == mode_e::TARGET ? "target" : "initiator"; + } + + string GetSignalName(int) const; + + InProcessBus &bus; + + bool log_signals = true; + + inline static const unordered_map SIGNALS { + { PIN_BSY, "BSY" }, + { PIN_SEL, "SEL" }, + { PIN_ATN, "ATN" }, + { PIN_ACK, "ACK" }, + { PIN_RST, "RST" }, + { PIN_MSG, "MSG" }, + { PIN_CD, "CD" }, + { PIN_IO, "IO" }, + { PIN_REQ, "REQ" } + }; +}; diff --git a/cpp/buses/pin_control.h b/cpp/buses/pin_control.h new file mode 100644 index 00000000..74496cf3 --- /dev/null +++ b/cpp/buses/pin_control.h @@ -0,0 +1,61 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022 akuker +// +//--------------------------------------------------------------------------- + +#include + +#pragma once + +// Methods that must be implemented by the derived gpiobus classes to control the GPIO pins +class PinControl +{ + +public: + + PinControl() = default; + virtual ~PinControl() = default; + + virtual bool GetBSY() const = 0; + virtual void SetBSY(bool) = 0; + + virtual bool GetSEL() const = 0; + virtual void SetSEL(bool) = 0; + + virtual bool GetATN() const = 0; + virtual void SetATN(bool) = 0; + + virtual bool GetACK() const = 0; + virtual void SetACK(bool) = 0; + + virtual bool GetRST() const = 0; + virtual void SetRST(bool) = 0; + + virtual bool GetMSG() const = 0; + virtual void SetMSG(bool) = 0; + + virtual bool GetCD() const = 0; + virtual void SetCD(bool) = 0; + + virtual bool GetIO() = 0; + virtual void SetIO(bool) = 0; + + virtual bool GetREQ() const = 0; + virtual void SetREQ(bool) = 0; + + virtual uint8_t GetDAT() = 0; + virtual void SetDAT(uint8_t) = 0; + + // GPIO pin direction setting + virtual void PinConfig(int, int) = 0; + + // GPIO pin pull up/down resistor setting + virtual void PullConfig(int, int) = 0; + + virtual void SetControl(int, bool) = 0; + + virtual void SetMode(int, int) = 0; +}; diff --git a/cpp/command_interface.proto b/cpp/command_interface.proto new file mode 100644 index 00000000..f69d00cc --- /dev/null +++ b/cpp/command_interface.proto @@ -0,0 +1,478 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +// +// Each message sent to the server is preceded by the magic string "RASCSI". +// A message starts with a little endian 32 bit header which contains the protobuf message size. +// Unless explicitly specified the order of repeated data returned is undefined. +// All operations accept an optional access token, specified by the "token" parameter. +// All operations also accept an optional locale, specified with the "locale" parameter. If there is +// a localized error message it is returned, with English being the fallback language. +// + +syntax = "proto3"; + +package command_interface; + +// The available device types +enum PbDeviceType { + UNDEFINED = 0; + // Non-removable SASI hard drive + SAHD = 1; + // Non-removable SCSI hard drive + SCHD = 2; + // Removable SCSI drive + SCRM = 3; + // Magnoto-Optical drive + SCMO = 4; + // CD-ROM drive + SCCD = 5; + // Network bridge + SCBR = 6 [deprecated = true]; + // DaynaPort network adapter + SCDP = 7; + // Host services device + SCHS = 8; + // Printer device + SCLP = 9; +} + +// Remote operations, returning PbResult +enum PbOperation { + NO_OPERATION = 0; + + // Attach devices and return the new device list (PbDevicesInfo) + // Parameters (depending on the device type): + // "file": The filename relative to the default image folder + // "interface": A prioritized comma-separated list of interfaces to create a network bridge for + // "inet": The IP address and netmask for the network bridge + // "cmd": The command to be used for printing, with "%f" as file placeholder + ATTACH = 1; + + // Detach a device and return the new device list (PbDevicesInfo) + // Detaches all LUNs of that device which are equal to or higher than the LUN specified. + DETACH = 2; + + // Detach all devices, does not require a device list + DETACH_ALL = 3; + + // Start device + START = 4; + + // Stop device, e.g. park drive + STOP = 5; + + // Insert medium + // Parameters: + // "file": The filename, relative to the default image folder + INSERT = 6; + + // Eject medium + EJECT = 7; + + // Write-protect medium (not possible for read-only media) + PROTECT = 8; + + // Make medium writable (not possible for read-only media) + UNPROTECT = 9; + + // Gets the server information (PbServerInfo). Calling this operation without a list of operations should + // be avoided because this may return a lot of data. More specific other operations should be used instead. + // Parameters: + // "operations": Optional case insensitive comma-separated list of operation names to return data for, + // e.g. "version_info,log_level_info". Unknown operation names are ignored. If this parameter is missing + // the full set of data supported by PbServerInfo is returned. + // "folder_pattern": Optional filter, only folder names containing the case-insensitive pattern are returned + // "file_pattern": Optional filter, only filenames containing the case-insensitive pattern are returned + SERVER_INFO = 10; + + // Get server version (PbVersionInfo) + VERSION_INFO = 11; + + // Get information on attached devices (PbDevicesInfo) + // Returns data for all attached devices if the device list is empty. + DEVICES_INFO = 12; + + // Get device properties by device type (PbDeviceTypesInfo) + DEVICE_TYPES_INFO = 13; + + // Get information on available image files in the default image folder (PbImageFilesInfo) + // Parameters: + // "folder_pattern": Optional filter, only folder names containing the case-insensitive pattern are returned + // "file_pattern": Optional filter, only filenames containing the case-insensitive pattern are returned + DEFAULT_IMAGE_FILES_INFO = 14; + + // Get information on an image file (not necessarily in the default image folder). + // Parameters: + // "file": The filename. Either an absolute path or a path relative to the default image folder. + IMAGE_FILE_INFO = 15; + + // Get information on the available log levels and the current log level (PbLogLevelInfo) + LOG_LEVEL_INFO = 16; + + // Get the names of the available network interfaces (PbNetworkInterfacesInfo) + // Only lists interfaces that are up. + NETWORK_INTERFACES_INFO = 17; + + // Get the mapping of extensions to device types (PbMappingInfo) + MAPPING_INFO = 18; + + // Get the list of reserved device IDs (PbReservedIdsInfo) + RESERVED_IDS_INFO = 19; + + // Set the default folder for image files. This folder must be located in /home. + // Parameters: + // "folder": The path of the default folder, relative to the user's home folder if relative. + DEFAULT_FOLDER = 20; + + // Set the log level. + // Parameters: + // "level": The new log level + LOG_LEVEL = 21; + + // Block IDs from being used, usually the IDs of the initiators (computers) in the SCSI chain. + // Parameters: + // "ids": A comma-separated list of IDs to reserve, or an empty string in order not to reserve any ID. + RESERVE_IDS = 22; + + // Shut down the s2p process or shut down/reboot the Pi + // Parameters: + // "mode": The shutdown mode, one of + // "rascsi": Shuts down the s2p process ("rascsi" instead of "s2p" for backwards compatibility) + // "system": Shuts down the Pi + // "reboot": Reboots the Pi + SHUT_DOWN = 23; + + // Create an image file. The image file must not yet exist. + // Parameters: + // "file": The filename, relative to the default image folder + // "size": The file size in bytes, must be a multiple of 512 + // "read_only": Optional, "true" (case-insensitive) in order to create a read-only file + CREATE_IMAGE = 24; + + // Delete an image file. + // Parameters: + // "file": The filename, relative to the default image folder. + DELETE_IMAGE = 25; + + // Rename/Move an image file. + // Parameters: + // "from": The old filename, relative to the default image folder + // "to": The new filename, relative to the default image folder + // The new filename must not yet exist. + RENAME_IMAGE = 26; + + // Copy an image file. + // Parameters: + // "from": The source filename, relative to the default image folder + // "to": The destination filename, relative to the default image folder + // "read_only": Optional, "true" (case-insensitive) in order to create a read-only file + // The destination filename must not yet exist. + COPY_IMAGE = 27; + + // Write-protect an image file. + // Parameters: + // "file": The filename, relative to the default image folder + PROTECT_IMAGE = 28; + + // Make an image file writable. + // Parameters: + // "file": The filename, relative to the default image folder + UNPROTECT_IMAGE = 29; + + // Check whether an authentication token is valid. A client can use this operation in order to find + // out whether authentication is enabled or to use authentication for securing client-internal operations. + CHECK_AUTHENTICATION = 30; + + // Get operation meta data (PbOperationInfo) + OPERATION_INFO = 31; + + // Get statistics (PbStatisticsInfo) + STATISTICS_INFO = 32; +} + +// The operation parameter meta data. The parameter data type is provided by the protobuf API. +message PbOperationParameter { + string name = 1; + // Optional short description + string description = 2; + // There is no specific set of permitted values if empty + repeated string permitted_values = 3; + // Optional default value + string default_value = 4; + // True if this parameter is mandatory + bool is_mandatory = 5; +} + +// The list of parameters supported by an operation +message PbOperationMetaData { + // The operation name at the time the server-side protobuf code was generated. + string server_side_name = 1; + // Optional short description + string description = 2; + repeated PbOperationParameter parameters = 3; +} + +// Mapping of operations to their ordinals +message PbOperationInfo { + map operations = 1; +} + +// Special purpose error codes for cases where a textual error message may not be not sufficient. +// The client may react on this code, e.g. by prompting the user for an authentication token. +enum PbErrorCode { + // No error code available + NO_ERROR_CODE = 0; + // The client sent an operation code not supported by this server + UNKNOWN_OPERATION = 1; + // Authentication/Authorization error + UNAUTHORIZED = 2; +} + +// The supported file extensions mapped to their respective device types +message PbMappingInfo { + map mapping = 1; +} + +// The properties supported by a device +message PbDeviceProperties { + // Read-only media (e.g. CD-ROMs) are not protectable but permanently read-only + bool read_only = 1; + // Medium can be write-protected + bool protectable = 2; + // Device can be stopped, e.g. parked + bool stoppable = 3; + // Medium can be removed + bool removable = 4; + // Medium can be locked + bool lockable = 5; + // Device supports image file as a parameter + bool supports_file = 6; + // Device supports parameters other than a filename + bool supports_params = 7; + // List of default parameters, if any (requires supports_params to be true) + map default_params = 8; + // LUN numbers this device can have, usually 32 + int32 luns = 9; + // Unordered list of permitted block sizes in bytes, empty if the block size is not configurable + repeated uint32 block_sizes = 10; +} + +// The status of a device, only meaningful if the respective feature is supported +message PbDeviceStatus { + // Medium is write-protected + bool protected = 1; + // Device is stopped, e.g. parked + bool stopped = 2; + // Medium is removed + bool removed = 3; + // Medium is locked + bool locked = 4; +} + +// Device properties by device type. (Note that a protobuf map cannot be used because enums cannot be map keys.) +message PbDeviceTypeProperties { + PbDeviceType type = 1; + PbDeviceProperties properties = 2; +} + +message PbDeviceTypesInfo { + repeated PbDeviceTypeProperties properties = 1; +} + +// The image file data +message PbImageFile { + string name = 1; + // The assumed device type, based on the filename extension + PbDeviceType type = 2; + // The file size in bytes, 0 for block devices in /dev + uint64 size = 3; + bool read_only = 4; +} + +// The default image folder and the image files it contains +message PbImageFilesInfo { + string default_image_folder = 1; + repeated PbImageFile image_files = 2; + // The maximum nesting depth, configured with the -R option + int32 depth = 3; +} + +// Log level information +message PbLogLevelInfo { + // List of available log levels, ordered by increasing by severity + repeated string log_levels = 2; + string current_log_level = 3; +} + +// The network interfaces information +message PbNetworkInterfacesInfo { + repeated string name = 1; +} + +// Statistics categories ordered by increasing severity +enum PbStatisticsCategory { + CATEGORY_NONE = 0; + CATEGORY_INFO = 1; + CATEGORY_WARNING = 2; + CATEGORY_ERROR = 3; + } + +message PbStatistics { + PbStatisticsCategory category = 1; + // The device ID and LUN for this statistics item. Both are -1 if the item is not device specific. + int32 id = 2; + int32 unit = 3; + // A symbolic unique item name, may be used for I18N. Supported values and their categories: + // "read_error_count" (ERROR, SCHD/SCRM/SCMO/SCCD) + // "write_error_count" (ERROR, SCHD/SCRM/SCMO) + // "cache_miss_read_count" (INFO, SCHD/SCRM/SCMO/SCCD) + // "cache_miss_write_count" (INFO, SCHD/SCRM/SCMO) + // "sector_read_count" (INFO, SCHD/SCRM/SCMO/SCCD) + // "sector_write_count" (INFO, SCHD/SCRM/SCMO) + // "byte_read_count" (INFO, SCDP) + // "byte_write_count" (INFO, SCDP) + // "print_error_count" (ERROR, SCLP) + // "print_warning_count" (WARNING, SCLP) + // "file_print_count" (INFO, SCLP) + // "byte_receive_count" (INFO, SCLP) + string key = 4; + uint64 value = 5; +} + +// The information on collected statistics +message PbStatisticsInfo { + repeated PbStatistics statistics = 1; +} + +// The device definition, sent from the client to the server +message PbDeviceDefinition { + int32 id = 1; + int32 unit = 2; + PbDeviceType type = 3; + // Device specific named parameters, e.g. the name of an image file + map params = 4; + // The optional block size in bytes per sector, must be one of the supported block sizes + int32 block_size = 5; + // The device name components + string vendor = 6; + string product = 7; + string revision = 8; + // Create a write-protected device + bool protected = 9; +} + +// The device data, sent from the server to the client +message PbDevice { + int32 id = 1; + int32 unit = 2; + PbDeviceType type = 3; + PbDeviceProperties properties = 4; + PbDeviceStatus status = 5; + // Image file information, if the device supports image files + PbImageFile file = 6; + // Effective parameters the device was created with + map params = 7; + string vendor = 8; + string product = 9; + string revision = 10; + // Block size in bytes + int32 block_size = 11; + // Number of blocks + uint64 block_count = 12; +} + +message PbDevicesInfo { + repeated PbDevice devices = 1; +} + +// The reserved device IDs +message PbReservedIdsInfo { + repeated int32 ids = 1; +} + +// Server version information +message PbVersionInfo { + // The server version + int32 major_version = 1; + int32 minor_version = 2; + // < 0 for a development version, = 0 if there is no patch yet + int32 patch_version = 3; + // The optional server identifier, e.g. "SCSI2Pi" + string identifier = 4; +} + +// Supported commands and their parameters +message PbCommand { + PbOperation operation = 1; + // The non-empty list of devices for this command + repeated PbDeviceDefinition devices = 2; + // The named parameters for the operation, e.g. a filename, or a network interface list + map params = 3; +} + +// The result of a command +message PbResult { + // false means that an error occurred + bool status = 1; + // An optional error or information message, depending on the status. A string without trailing CR/LF. + string msg = 2; + // An optional error code. Only to be used in cases where textual information is not sufficient. + PbErrorCode error_code = 14; + // Optional additional result data + oneof result { + // The result of a SERVER_INFO command + PbServerInfo server_info = 3; + // The result of a VERSION_INFO command + PbVersionInfo version_info = 4; + // The result of a LOG_LEVEL_INFO command + PbLogLevelInfo log_level_info = 5; + // The result of a DEVICES_INFO, ATTACH or DETACH command + PbDevicesInfo devices_info = 6; + // The result of a DEVICE_TYPES_INFO command + PbDeviceTypesInfo device_types_info = 7; + // The result of a DEFAULT_IMAGE_FILES_INFO command + PbImageFilesInfo image_files_info = 8; + // The result of an IMAGE_FILE_INFO command + PbImageFile image_file_info = 9; + // The result of a NETWORK_INTERFACES_INFO command + PbNetworkInterfacesInfo network_interfaces_info = 10; + // The result of an MAPPING_INFO command + PbMappingInfo mapping_info = 11; + // The result of a RESERVED_IDS_INFO command + PbReservedIdsInfo reserved_ids_info = 12; + // The result of an OPERATION_INFO command + PbOperationInfo operation_info = 13; + // The result of a STATISTICS_INFO command + PbStatisticsInfo statistics_info = 15; + } +} + +// The server information. All data can also be requested with individual commands, which takes less bandwith. +message PbServerInfo { + // The server version data + PbVersionInfo version_info = 1; + // The available log levels and the current log level + PbLogLevelInfo log_level_info = 2; + // Supported device types and their properties + PbDeviceTypesInfo device_types_info = 3; + // The image files in the default folder + PbImageFilesInfo image_files_info = 4; + // The available (up) network interfaces + PbNetworkInterfacesInfo network_interfaces_info = 5; + // The extensions to device types mapping + PbMappingInfo mapping_info = 6; + // The reserved device IDs + PbReservedIdsInfo reserved_ids_info = 7; + // The attached devices + PbDevicesInfo devices_info = 8; + // The operation meta data + PbOperationInfo operation_info = 9; + // The statistics + PbStatisticsInfo statistics_info = 10; +} diff --git a/cpp/controllers/abstract_controller.cpp b/cpp/controllers/abstract_controller.cpp new file mode 100644 index 00000000..568d5553 --- /dev/null +++ b/cpp/controllers/abstract_controller.cpp @@ -0,0 +1,139 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/shared_exceptions.h" +#include "base/primary_device.h" +#include "abstract_controller.h" +#include +#include + +using namespace scsi_defs; + +AbstractController::AbstractController(BUS& bus, int target_id, int max_luns) : bus(bus), target_id(target_id), max_luns(max_luns) +{ + // The initial buffer size is the size of the biggest supported sector + ctrl.buffer.resize(4096); + + device_logger.SetIdAndLun(target_id, -1); +} + +void AbstractController::AllocateCmd(size_t size) +{ + if (size > ctrl.cmd.size()) { + LogTrace(fmt::format("Resizing transfer buffer to {} bytes", size)); + ctrl.cmd.resize(size); + } +} + +void AbstractController::SetLength(size_t length) +{ + if (length > ctrl.buffer.size()) { + ctrl.buffer.resize(length); + } + + ctrl.length = length; +} + +void AbstractController::CopyToBuffer(const void *src, size_t size) //NOSONAR Any kind of source data is permitted +{ + SetLength(size); + + memcpy(ctrl.buffer.data(), src, size); +} + +void AbstractController::SetByteTransfer(bool b) +{ + is_byte_transfer = b; + + if (!is_byte_transfer) { + bytes_to_transfer = 0; + } +} + +unordered_set> AbstractController::GetDevices() const +{ + unordered_set> devices; + + // "luns | views:values" is not supported by the bullseye compiler + ranges::transform(luns, inserter(devices, devices.begin()), [] (const auto& l) { return l.second; } ); + + return devices; +} + +shared_ptr AbstractController::GetDeviceForLun(int lun) const { + const auto& it = luns.find(lun); + return it == luns.end() ? nullptr : it->second; +} + +void AbstractController::Reset() +{ + SetPhase(phase_t::busfree); + + ctrl = {}; + + SetByteTransfer(false); + + // Reset all LUNs + for (const auto& [_, device] : luns) { + device->Reset(); + } + + GetBus().Reset(); +} + +void AbstractController::ProcessOnController(int id_data) +{ + device_logger.SetIdAndLun(GetTargetId(), -1); + + const int initiator_id = ExtractInitiatorId(id_data); + if (initiator_id != UNKNOWN_INITIATOR_ID) { + LogTrace("++++ Starting processing for initiator ID " + to_string(initiator_id)); + } + else { + LogTrace("++++ Starting processing for unknown initiator ID"); + } + + while (Process(initiator_id)) { + // Handle bus phases until the bus is free for the next command + } +} + +bool AbstractController::AddDevice(shared_ptr device) +{ + const int lun = device->GetLun(); + + if (lun < 0 || lun >= GetMaxLuns() || HasDeviceForLun(lun) || device->GetController()) { + return false; + } + + luns[lun] = device; + device->SetController(this); + + return true; +} + +bool AbstractController::RemoveDevice(PrimaryDevice& device) +{ + device.CleanUp(); + + return luns.erase(device.GetLun()) == 1; +} + +bool AbstractController::HasDeviceForLun(int lun) const +{ + return luns.contains(lun); +} + +int AbstractController::ExtractInitiatorId(int id_data) const +{ + if (const int id_data_without_target = id_data - (1 << target_id); id_data_without_target) { + return static_cast(log2(id_data_without_target & -id_data_without_target)); + } + + return UNKNOWN_INITIATOR_ID; +} diff --git a/cpp/controllers/abstract_controller.h b/cpp/controllers/abstract_controller.h new file mode 100644 index 00000000..719060ec --- /dev/null +++ b/cpp/controllers/abstract_controller.h @@ -0,0 +1,150 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +// Base class for device controllers +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "buses/bus.h" +#include "phase_handler.h" +#include "base/device_logger.h" +#include +#include +#include +#include + +using namespace std; + +class PrimaryDevice; + +class AbstractController : public PhaseHandler +{ + +public: + + static inline const int UNKNOWN_INITIATOR_ID = -1; + + enum class shutdown_mode { + NONE, + STOP_S2P, + STOP_PI, + RESTART_PI + }; + + AbstractController(BUS&, int, int); + ~AbstractController() override = default; + + virtual void Error(scsi_defs::sense_key, scsi_defs::asc = scsi_defs::asc::no_additional_sense_information, + scsi_defs::status = scsi_defs::status::check_condition) = 0; + virtual void Reset(); + virtual int GetInitiatorId() const = 0; + + virtual int GetEffectiveLun() const = 0; + + void ScheduleShutdown(shutdown_mode mode) { sh_mode = mode; } + shutdown_mode GetShutdownMode() const { return sh_mode; } + + int GetTargetId() const { return target_id; } + int GetMaxLuns() const { return max_luns; } + int GetLunCount() const { return static_cast(luns.size()); } + + unordered_set> GetDevices() const; + shared_ptr GetDeviceForLun(int) const; + bool AddDevice(shared_ptr); + bool RemoveDevice(PrimaryDevice&); + bool HasDeviceForLun(int) const; + void ProcessOnController(int); + + void CopyToBuffer(const void *, size_t); + auto& GetBuffer() { return ctrl.buffer; } + auto GetStatus() const { return ctrl.status; } + void SetStatus(scsi_defs::status s) { ctrl.status = s; } + auto GetLength() const { return ctrl.length; } + void SetLength(size_t); + void SetBlocks(uint32_t b) { ctrl.blocks = b; } + void SetNext(uint64_t n) { ctrl.next = n; } + void SetMessage(int m) { ctrl.message = m; } + auto GetCmd() const { return ctrl.cmd; } + int GetCmdByte(int index) const { return ctrl.cmd[index]; } + void SetByteTransfer(bool); + +protected: + + inline BUS& GetBus() const { return bus; } + + auto GetOpcode() const { return static_cast(ctrl.cmd[0]); } + int GetLun() const { return (ctrl.cmd[1] >> 5) & 0x07; } + + void AllocateCmd(size_t); + + void SetCmdByte(int index, int value) { ctrl.cmd[index] = value; } + + bool HasBlocks() const { return ctrl.blocks; } + void DecrementBlocks() { --ctrl.blocks; } + auto GetNext() const { return ctrl.next; } + void IncrementNext() { ++ctrl.next; } + int GetMessage() const { return ctrl.message; } + + bool HasValidLength() const { return ctrl.length != 0; } + int GetOffset() const { return ctrl.offset; } + void ResetOffset() { ctrl.offset = 0; } + void UpdateOffsetAndLength() { ctrl.offset += ctrl.length; ctrl.length = 0; } + + bool IsByteTransfer() const { return is_byte_transfer; } + void InitBytesToTransfer() { bytes_to_transfer = ctrl.length; } + auto GetBytesToTransfer() const { return bytes_to_transfer; } + + void LogTrace(const string& s) const { device_logger.Trace(s); } + void LogDebug(const string& s) const { device_logger.Debug(s); } + void LogInfo(const string& s) const { device_logger.Info(s); } + void LogWarn(const string& s) const { device_logger.Warn(s); } + void LogError(const string& s) const { device_logger.Error(s); } + +private: + + int ExtractInitiatorId(int) const; + + using ctrl_t = struct _ctrl_t { + // Command data, dynamically resized if required + vector cmd = vector(16); + + // Status data + scsi_defs::status status; + // Message data + int message; + + // Transfer data buffer, dynamically resized if required + vector buffer; + // Number of transfer blocks + uint32_t blocks; + // Next record + uint64_t next; + // Transfer offset + uint32_t offset; + // Remaining bytes to be transferred + uint32_t length; + }; + + ctrl_t ctrl = {}; + + BUS& bus; + + DeviceLogger device_logger; + + // Logical units of this controller mapped to their LUN numbers + unordered_map> luns; + + int target_id; + + int max_luns; + + bool is_byte_transfer = false; + uint32_t bytes_to_transfer = 0; + + shutdown_mode sh_mode = shutdown_mode::NONE; +}; diff --git a/cpp/controllers/controller_factory.cpp b/cpp/controllers/controller_factory.cpp new file mode 100644 index 00000000..32e36ffd --- /dev/null +++ b/cpp/controllers/controller_factory.cpp @@ -0,0 +1,120 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "base/primary_device.h" +#include "scsi_controller.h" +#include "sasi_controller.h" +#include "controller_factory.h" + +using namespace std; + +shared_ptr ControllerFactory::CreateController(BUS& bus, int id) const +{ + shared_ptr controller; + if (is_sasi) { + controller = make_shared(bus, id, GetSasiLunMax()); + } + else { + controller = make_shared(bus, id, GetScsiLunMax()); + } + + controller->Init(); + + return controller; +} + +bool ControllerFactory::AttachToController(BUS& bus, int id, shared_ptr device) +{ + if (auto controller = FindController(id); controller) { + if (device->GetLun() > GetLunMax() || controller->HasDeviceForLun(device->GetLun())) { + return false; + } + + return controller->AddDevice(device); + } + + // If this is LUN 0 create a new controller + if (!device->GetLun()) { + if (auto controller = CreateController(bus, id); controller->AddDevice(device)) { + controllers[id] = controller; + + return true; + } + } + + return false; +} + +bool ControllerFactory::DeleteController(const AbstractController& controller) +{ + for (const auto& device : controller.GetDevices()) { + device->CleanUp(); + } + + return controllers.erase(controller.GetTargetId()) == 1; +} + +void ControllerFactory::DeleteAllControllers() +{ + unordered_set> values; + ranges::transform(controllers, inserter(values, values.begin()), [] (const auto& controller) { return controller.second; } ); + + for (const auto& controller : values) { + DeleteController(*controller); + } + + assert(controllers.empty()); +} + +AbstractController::shutdown_mode ControllerFactory::ProcessOnController(int id_data) const +{ + if (const auto& it = ranges::find_if(controllers, [&] (const auto& c) { return (id_data & (1 << c.first)); } ); + it != controllers.end()) { + (*it).second->ProcessOnController(id_data); + + return (*it).second->GetShutdownMode(); + } + + return AbstractController::shutdown_mode::NONE; +} + +shared_ptr ControllerFactory::FindController(int target_id) const +{ + const auto& it = controllers.find(target_id); + return it == controllers.end() ? nullptr : it->second; +} + +bool ControllerFactory::HasController(int target_id) const { + return controllers.contains(target_id); +} + +unordered_set> ControllerFactory::GetAllDevices() const +{ + unordered_set> devices; + + for (const auto& [_, controller] : controllers) { + const auto& d = controller->GetDevices(); + devices.insert(d.begin(), d.end()); + } + + return devices; +} + +bool ControllerFactory::HasDeviceForIdAndLun(int id, int lun) const +{ + return GetDeviceForIdAndLun(id, lun) != nullptr; +} + +shared_ptr ControllerFactory::GetDeviceForIdAndLun(int id, int lun) const +{ + if (const auto& controller = FindController(id); controller) { + return controller->GetDeviceForLun(lun); + } + + return nullptr; +} diff --git a/cpp/controllers/controller_factory.h b/cpp/controllers/controller_factory.h new file mode 100644 index 00000000..7b906f2a --- /dev/null +++ b/cpp/controllers/controller_factory.h @@ -0,0 +1,53 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "buses/bus.h" +#include "controllers/abstract_controller.h" +#include +#include +#include + +using namespace std; + +class PrimaryDevice; + +class ControllerFactory +{ + +public: + + explicit ControllerFactory(bool b = false) { is_sasi = b; } + ~ControllerFactory() = default; + + bool AttachToController(BUS&, int, shared_ptr); + bool DeleteController(const AbstractController&); + void DeleteAllControllers(); + AbstractController::shutdown_mode ProcessOnController(int) const; + shared_ptr FindController(int) const; + bool HasController(int) const; + unordered_set> GetAllDevices() const; + bool HasDeviceForIdAndLun(int, int) const; + shared_ptr GetDeviceForIdAndLun(int, int) const; + + static int GetIdMax() { return 8; } + static int GetLunMax() { return is_sasi ? GetSasiLunMax() : GetScsiLunMax(); } + static int GetScsiLunMax() { return 32; } + static int GetSasiLunMax() { return 2; } + +private: + + shared_ptr CreateController(BUS&, int) const; + + // Controllers mapped to their device IDs + unordered_map> controllers; + + // TODO Try to make non-static + inline static bool is_sasi = false; +}; diff --git a/cpp/controllers/generic_controller.cpp b/cpp/controllers/generic_controller.cpp new file mode 100644 index 00000000..78ee9e51 --- /dev/null +++ b/cpp/controllers/generic_controller.cpp @@ -0,0 +1,743 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +// Copyright (C) 2014-2020 GIMONS +// Copyright (C) akuker +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/shared_exceptions.h" +#include "buses/gpiobus.h" +#include "devices/disk.h" +#ifdef BUILD_SCDP +#include "devices/daynaport.h" +#endif +#include "generic_controller.h" + +using namespace scsi_defs; + +void GenericController::Reset() +{ + AbstractController::Reset(); + + initiator_id = UNKNOWN_INITIATOR_ID; +} + +bool GenericController::Process(int id) +{ + GetBus().Acquire(); + + if (GetBus().GetRST()) { + LogWarn("RESET signal received"); + Reset(); + return false; + } + + initiator_id = id; + + if (!ProcessPhase()) { + Error(sense_key::aborted_command); + return false; + } + + return !IsBusFree(); +} + +void GenericController::BusFree() +{ + if (!IsBusFree()) { + LogTrace("Bus Free phase"); + SetPhase(phase_t::busfree); + + GetBus().SetREQ(false); + GetBus().SetMSG(false); + GetBus().SetCD(false); + GetBus().SetIO(false); + GetBus().SetBSY(false); + + // Initialize status and message + SetStatus(status::good); + SetMessage(0x00); + + SetByteTransfer(false); + + return; + } + + // Move to selection phase + if (GetBus().GetSEL() && !GetBus().GetBSY()) { + Selection(); + } +} + +void GenericController::Selection() +{ + if (!IsSelection()) { + LogTrace("Selection phase"); + SetPhase(phase_t::selection); + + GetBus().SetBSY(true); + return; + } + + if (!GetBus().GetSEL() && GetBus().GetBSY()) { + LogTrace("Selection completed"); + + // Message out phase if ATN=1, otherwise command phase + if (GetBus().GetATN()) { + MsgOut(); + } else { + Command(); + } + } +} + +void GenericController::Command() +{ + if (!IsCommand()) { + LogTrace("Command phase"); + SetPhase(phase_t::command); + + GetBus().SetMSG(false); + GetBus().SetCD(true); + GetBus().SetIO(false); + + const int actual_count = GetBus().CommandHandShake(GetBuffer()); + if (!actual_count) { + LogTrace(fmt::format("Received unknown command: ${:02x}", static_cast(GetBuffer()[0]))); + + Error(sense_key::illegal_request, asc::invalid_command_operation_code); + return; + } + + const int command_byte_count = BUS::GetCommandByteCount(static_cast(GetBuffer()[0])); + + AllocateCmd(command_byte_count); + for (int i = 0; i < command_byte_count; i++) { + SetCmdByte(i, GetBuffer()[i]); + } + + if (spdlog::get_level() <= spdlog::level::debug) { + string s = fmt::format("Controller is executing {}, CDB $", + command_mapping.find(GetOpcode())->second.second); + for (int i = 0; i < BUS::GetCommandByteCount(static_cast(GetOpcode())); i++) { + s += fmt::format("{:02x}", GetCmdByte(i)); + } + LogDebug(s); + } + + if (actual_count != command_byte_count) { + LogError(fmt::format( + "Command byte count mismatch for command ${0:02x}: expected {1} bytes, received {2} byte(s)", + static_cast(GetOpcode()), command_byte_count, actual_count)); + Error(sense_key::illegal_request, asc::invalid_field_in_cdb); + return; + } + + SetLength(0); + + Execute(); + } +} + +void GenericController::Execute() +{ + // Initialization for data transfer + ResetOffset(); + SetBlocks(1); + + // Discard pending sense data from the previous command if the current command is not REQUEST SENSE + if (GetOpcode() != scsi_command::cmd_request_sense) { + SetStatus(status::good); + } + + int lun = GetEffectiveLun(); + if (!HasDeviceForLun(lun)) { + if (GetOpcode() != scsi_command::cmd_inquiry && GetOpcode() != scsi_command::cmd_request_sense) { + LogTrace(fmt::format("Invalid LUN {}", lun)); + + Error(sense_key::illegal_request, asc::invalid_lun); + + return; + } + + assert(HasDeviceForLun(0)); + + lun = 0; + } + + // SCSI-2 4.4.3 Incorrect logical unit handling + if (GetOpcode() == scsi_command::cmd_inquiry && !HasDeviceForLun(lun)) { + LogTrace(fmt::format("Reporting LUN {} as not supported", GetEffectiveLun())); + + GetBuffer().data()[0] = 0x7f; + + return; + } + + auto device = GetDeviceForLun(lun); + + // Discard pending sense data from the previous command if the current command is not REQUEST SENSE + if (GetOpcode() != scsi_command::cmd_request_sense) { + device->SetStatusCode(0); + } + + if (device->CheckReservation(initiator_id, GetOpcode(), GetCmdByte(4) & 0x01)) { + try { + device->Dispatch(GetOpcode()); + } + catch(const scsi_exception& e) { + Error(e.get_sense_key(), e.get_asc()); + } + } + else { + Error(sense_key::aborted_command, asc::no_additional_sense_information, status::reservation_conflict); + } +} + +void GenericController::Status() +{ + if (!IsStatus()) { + LogTrace(fmt::format("Status phase, status is ${:02x}", static_cast(GetStatus()))); + SetPhase(phase_t::status); + + // Signal line operated by the target + GetBus().SetMSG(false); + GetBus().SetCD(true); + GetBus().SetIO(true); + + // Data transfer is 1 byte x 1 block + ResetOffset(); + SetLength(1); + SetBlocks(1); + GetBuffer()[0] = (uint8_t)GetStatus(); + + return; + } + + Send(); +} + +void GenericController::MsgIn() +{ + if (!IsMsgIn()) { + LogTrace("Message In phase"); + SetPhase(phase_t::msgin); + + GetBus().SetMSG(true); + GetBus().SetCD(true); + GetBus().SetIO(true); + + ResetOffset(); + return; + } + + Send(); +} + +void GenericController::DataIn() +{ + if (!IsDataIn()) { + if (!HasValidLength()) { + Status(); + return; + } + + LogTrace("Data In phase"); + SetPhase(phase_t::datain); + + GetBus().SetMSG(false); + GetBus().SetCD(false); + GetBus().SetIO(true); + + ResetOffset(); + + return; + } + + Send(); +} + +void GenericController::DataOut() +{ + if (!IsDataOut()) { + if (!HasValidLength()) { + Status(); + return; + } + + LogTrace("Data Out phase"); + SetPhase(phase_t::dataout); + + GetBus().SetMSG(false); + GetBus().SetCD(false); + GetBus().SetIO(false); + + ResetOffset(); + return; + } + + Receive(); +} + +void GenericController::Error(sense_key sense_key, asc asc, status status) +{ + GetBus().Acquire(); + + if (GetBus().GetRST()) { + BusFree(); + return; + } + + // Bus free for status phase and message in phase + if (IsStatus() || IsMsgIn()) { + BusFree(); + return; + } + + int lun = GetEffectiveLun(); + if (!HasDeviceForLun(lun) || asc == asc::invalid_lun) { + if (!HasDeviceForLun(0)) { + LogError("No LUN 0"); + + SetStatus(status); + SetMessage(0x00); + + Status(); + + return; + } + + lun = 0; + } + + if (sense_key != sense_key::no_sense || asc != asc::no_additional_sense_information) { + LogDebug(fmt::format("Error status: Sense Key ${0:02x}, ASC ${1:02x}", static_cast(sense_key), + static_cast(asc))); + + // Set Sense Key and ASC for a subsequent REQUEST SENSE + GetDeviceForLun(lun)->SetStatusCode((static_cast(sense_key) << 16) | (static_cast(asc) << 8)); + } + + SetStatus(status); + SetMessage(0x00); + + Status(); +} + +void GenericController::Send() +{ + assert(!GetBus().GetREQ()); + assert(GetBus().GetIO()); + + if (HasValidLength()) { + LogTrace(fmt::format("Sending data, offset: {0}, length: {1}", GetOffset(), GetLength())); + + // The delay should be taken from the respective LUN, but as there are no Daynaport drivers for + // LUNs other than 0 this work-around works. + if (const int len = GetBus().SendHandShake(GetBuffer().data() + GetOffset(), GetLength(), + HasDeviceForLun(0) ? GetDeviceForLun(0)->GetSendDelay() : 0); + len != static_cast(GetLength())) { + // If you cannot send all, move to status phase + Error(sense_key::aborted_command); + return; + } + + UpdateOffsetAndLength(); + + return; + } + + DecrementBlocks(); + + // Processing after data collection (read/data-in only) + if (IsDataIn() && HasBlocks()) { + // Set next buffer (set offset, length) + if (!XferIn(GetBuffer())) { + // Move to status phase + Error(sense_key::aborted_command); + return; + } + + LogTrace("Processing after data collection"); + } + + // Continue sending if blocks != 0 + if (HasBlocks()) { + LogTrace("Continuing to send"); + assert(HasValidLength()); + assert(GetOffset() == 0); + return; + } + + LogTrace("All data transferred"); + + // Move to next phase + switch (GetPhase()) { + case phase_t::msgin: + ProcessExtendedMessage(); + break; + + case phase_t::datain: + Status(); + break; + + case phase_t::status: + SetLength(1); + SetBlocks(1); + GetBuffer()[0] = (uint8_t)GetMessage(); + MsgIn(); + break; + + default: + assert(false); + break; + } +} + +void GenericController::Receive() +{ + assert(!GetBus().GetREQ()); + assert(!GetBus().GetIO()); + + if (HasValidLength()) { + LogTrace(fmt::format("Receiving {} byte(s)", GetLength())); + + // If not able to receive all, move to status phase + if (uint32_t len = GetBus().ReceiveHandShake(GetBuffer().data() + GetOffset(), GetLength()); len != GetLength()) { + LogError(fmt::format("Not able to receive {0} byte(s), only received {1}", GetLength(), len)); + Error(sense_key::aborted_command); + return; + } + } + + if (IsByteTransfer()) { + ReceiveBytes(); + return; + } + + if (HasValidLength()) { + UpdateOffsetAndLength(); + return; + } + + DecrementBlocks(); + bool result = true; + + // Processing after receiving data (by phase) + switch (GetPhase()) { + case phase_t::dataout: + if (!HasBlocks()) { + // End with this buffer + result = XferOut(false); + } else { + // Continue to next buffer (set offset, length) + result = XferOut(true); + } + break; + + case phase_t::msgout: + SetMessage(GetBuffer()[0]); + if (!XferMsg(GetMessage())) { + // Immediately free the bus if message output fails + BusFree(); + return; + } + + // Clear message data in preparation for message-in + SetMessage(0x00); + break; + + default: + break; + } + + // If result FALSE, move to status phase + if (!result) { + Error(sense_key::aborted_command); + return; + } + + // Continue to receive if blocks != 0 + if (HasBlocks()) { + assert(HasValidLength()); + assert(GetOffset() == 0); + return; + } + + // Move to next phase + switch (GetPhase()) { + case phase_t::command: + ProcessCommand(); + break; + + case phase_t::msgout: + ProcessMessage(); + break; + + case phase_t::dataout: + // Block-oriented data have been handled above + DataOutNonBlockOriented(); + + Status(); + break; + + default: + assert(false); + break; + } +} + +void GenericController::ReceiveBytes() +{ + if (HasValidLength()) { + InitBytesToTransfer(); + UpdateOffsetAndLength(); + return; + } + + bool result = true; + + // Processing after receiving data (by phase) + switch (GetPhase()) { + case phase_t::dataout: + result = XferOut(false); + break; + + case phase_t::msgout: + SetMessage(GetBuffer()[0]); + if (!XferMsg(GetMessage())) { + // Immediately free the bus if message output fails + BusFree(); + return; + } + + // Clear message data in preparation for message-in + SetMessage(0x00); + break; + + default: + break; + } + + // If result FALSE, move to status phase + if (!result) { + Error(sense_key::aborted_command); + return; + } + + // Move to next phase + switch (GetPhase()) { + case phase_t::command: + ProcessCommand(); + break; + + case phase_t::msgout: + ProcessMessage(); + break; + + case phase_t::dataout: + Status(); + break; + + default: + assert(false); + break; + } +} + +bool GenericController::XferOut(bool cont) +{ + assert(IsDataOut()); + + if (!IsByteTransfer()) { + return XferOutBlockOriented(cont); + } + + const uint32_t count = GetBytesToTransfer(); + SetByteTransfer(false); + + auto device = GetDeviceForLun(GetEffectiveLun()); + return device ? device->WriteByteSequence(span(GetBuffer().data(), count)) : false; +} + +void GenericController::DataOutNonBlockOriented() const +{ + assert(IsDataOut()); + + switch (GetOpcode()) { + case scsi_command::cmd_write6: + case scsi_command::cmd_write10: + case scsi_command::cmd_write16: + case scsi_command::cmd_write_long10: + case scsi_command::cmd_write_long16: + case scsi_command::cmd_verify10: + case scsi_command::cmd_verify16: + case scsi_command::cmd_mode_select6: + case scsi_command::cmd_mode_select10: + break; + + case scsi_command::cmd_set_mcast_addr: + // TODO: Eventually, we should store off the multicast address configuration data here... + break; + + default: + LogWarn(fmt::format("Unexpected Data Out phase for command ${:02x}", static_cast(GetOpcode()))); + break; + } +} + +bool GenericController::XferIn(vector& buf) +{ + assert(IsDataIn()); + + LogTrace(fmt::format("Command: ${:02x}", static_cast(GetOpcode()))); + + const int lun = GetEffectiveLun(); + if (!HasDeviceForLun(lun)) { + return false; + } + + // Limited to read commands + switch (GetOpcode()) { + case scsi_command::cmd_read6: + case scsi_command::cmd_read10: + case scsi_command::cmd_read16: +#ifdef BUILD_DISK + try { + SetLength(dynamic_pointer_cast(GetDeviceForLun(lun))->Read(buf, GetNext())); + } + catch(const scsi_exception&) { + // If there is an error, go to the status phase + return false; + } + + IncrementNext(); + + // If things are normal, work setting + ResetOffset(); +#endif + break; + + default: + assert(false); + return false; + } + + return true; +} + +bool GenericController::XferOutBlockOriented(bool cont) +{ + auto device = GetDeviceForLun(GetEffectiveLun()); + + // Limited to write commands + switch (GetOpcode()) { + case scsi_command::cmd_mode_select6: + case scsi_command::cmd_mode_select10: + { +#ifdef BUILD_MODE_PAGE_DEVICE + auto mode_page_device = dynamic_pointer_cast(device); + if (!mode_page_device) { + return false; + } + + try { + mode_page_device->ModeSelect(GetOpcode(), GetCmd(), GetBuffer(), GetOffset()); + } + catch(const scsi_exception& e) { + Error(e.get_sense_key(), e.get_asc()); + return false; + } +#endif + break; + } + + case scsi_command::cmd_write6: + case scsi_command::cmd_write10: + case scsi_command::cmd_write16: + { +#ifdef BUILD_SCDP + // TODO Get rid of this special case for SCDP + if (auto daynaport = dynamic_pointer_cast(device); daynaport) { + if (!daynaport->Write(GetCmd(), GetBuffer())) { + return false; + } + + ResetOffset(); + break; + } +#endif + +#ifdef BUILD_DISK + auto disk = dynamic_pointer_cast(device); + if (!disk) { + return false; + } + + try { + disk->Write(GetBuffer(), GetNext() - 1); + } + catch(const scsi_exception& e) { + Error(e.get_sense_key(), e.get_asc()); + + return false; + } + + // If you do not need the next block, end here + IncrementNext(); + if (cont) { + SetLength(disk->GetSectorSizeInBytes()); + ResetOffset(); + } +#endif + break; + } + + case scsi_command::cmd_verify10: + case scsi_command::cmd_verify16: + { +#ifdef BUILD_DISK + auto disk = dynamic_pointer_cast(device); + if (!disk) { + return false; + } + + // If you do not need the next block, end here + IncrementNext(); + if (cont) { + SetLength(disk->GetSectorSizeInBytes()); + ResetOffset(); + } +#endif + break; + } + + case scsi_command::cmd_set_mcast_addr: + LogTrace("Done with DaynaPort Set Multicast Address"); + break; + + default: + LogWarn(fmt::format("Received unexpected command ${:02x}", static_cast(GetOpcode()))); + break; + } + + return true; +} + +void GenericController::ProcessCommand() +{ + const uint32_t len = GPIOBUS::GetCommandByteCount(GetBuffer()[0]); + + string s = "CDB=$"; + for (uint32_t i = 0; i < len; i++) { + SetCmdByte(i, GetBuffer()[i]); + s += fmt::format("{:02x}", GetCmdByte(i)); + } + LogTrace(s); + + Execute(); +} diff --git a/cpp/controllers/generic_controller.h b/cpp/controllers/generic_controller.h new file mode 100644 index 00000000..15843910 --- /dev/null +++ b/cpp/controllers/generic_controller.h @@ -0,0 +1,67 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +// Abstract base class for SCSI-like controllers +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "abstract_controller.h" + +using namespace std; + +class GenericController : public AbstractController +{ + +public: + + GenericController(BUS& bus, int target_id, int lun_max) : AbstractController(bus, target_id, lun_max) { } + ~GenericController() override = default; + + virtual void Reset() override; + + bool Process(int) override; + + void Error(scsi_defs::sense_key sense_key, scsi_defs::asc asc = scsi_defs::asc::no_additional_sense_information, + scsi_defs::status status = scsi_defs::status::check_condition) override; + + int GetInitiatorId() const override { return initiator_id; } + + void BusFree() override; + void Selection() override; + void Command() override; + void MsgIn() override; + void Status() override; + void DataIn() override; + void DataOut() override; + +protected: + + bool XferIn(vector&); + bool XferOut(bool); + void Receive(); + + void Execute(); + void DataOutNonBlockOriented() const; + void ProcessCommand(); + + virtual void ParseMessage() = 0; + virtual void ProcessMessage() = 0; + virtual void ProcessExtendedMessage() = 0; + +private: + + void Send(); + virtual bool XferMsg(int) = 0; + bool XferOutBlockOriented(bool); + void ReceiveBytes(); + + + // The initiator ID may be unavailable, e.g. with Atari ACSI and old host adapters + int initiator_id = UNKNOWN_INITIATOR_ID; +}; + diff --git a/cpp/controllers/phase_handler.cpp b/cpp/controllers/phase_handler.cpp new file mode 100644 index 00000000..21e2d340 --- /dev/null +++ b/cpp/controllers/phase_handler.cpp @@ -0,0 +1,21 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "phase_handler.h" + +void PhaseHandler::Init() +{ + phase_executors[phase_t::busfree] = [this] () { BusFree(); }; + phase_executors[phase_t::selection] = [this] () { Selection(); }; + phase_executors[phase_t::dataout] = [this] () { DataOut(); }; + phase_executors[phase_t::datain] = [this] () { DataIn(); }; + phase_executors[phase_t::command] = [this] () { Command(); }; + phase_executors[phase_t::status] = [this] () { Status(); }; + phase_executors[phase_t::msgout] = [this] () { MsgOut(); }; + phase_executors[phase_t::msgin] = [this] () { MsgIn(); }; +} diff --git a/cpp/controllers/phase_handler.h b/cpp/controllers/phase_handler.h new file mode 100644 index 00000000..36bdea46 --- /dev/null +++ b/cpp/controllers/phase_handler.h @@ -0,0 +1,68 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "shared/scsi.h" +#include "shared/shared_exceptions.h" +#include +#include + +using namespace scsi_defs; + +class PhaseHandler +{ + phase_t phase = phase_t::busfree; + +public: + + PhaseHandler() = default; + virtual ~PhaseHandler() = default; + + void Init(); + + virtual void BusFree() = 0; + virtual void Selection() = 0; + virtual void Command() = 0; + virtual void Status() = 0; + virtual void DataIn() = 0; + virtual void DataOut() = 0; + virtual void MsgIn() = 0; + virtual void MsgOut() { } + + virtual bool Process(int) = 0; + +protected: + + phase_t GetPhase() const { return phase; } + void SetPhase(phase_t p) { phase = p; } + bool IsSelection() const { return phase == phase_t::selection; } + bool IsBusFree() const { return phase == phase_t::busfree; } + bool IsCommand() const { return phase == phase_t::command; } + bool IsStatus() const { return phase == phase_t::status; } + bool IsDataIn() const { return phase == phase_t::datain; } + bool IsDataOut() const { return phase == phase_t::dataout; } + bool IsMsgIn() const { return phase == phase_t::msgin; } + bool IsMsgOut() const { return phase == phase_t::msgout; } + + bool ProcessPhase() const + { + try { + phase_executors.at(phase)(); + } + catch(const out_of_range&) { + return false; + } + + return true; + } + +private: + + unordered_map> phase_executors; +}; diff --git a/cpp/controllers/sasi_controller.cpp b/cpp/controllers/sasi_controller.cpp new file mode 100644 index 00000000..3a35be88 --- /dev/null +++ b/cpp/controllers/sasi_controller.cpp @@ -0,0 +1,41 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "sasi_controller.h" + +void SasiController::MsgOut() +{ + // SASI does not support a message out phase + Command(); +} + +bool SasiController::XferMsg(int) +{ + assert(false); + return false; +} + +void SasiController::ParseMessage() +{ + assert(false); +} + +void SasiController::ProcessMessage() +{ + assert(false); +} + +void SasiController::ProcessExtendedMessage() +{ + BusFree(); +} + +int SasiController::GetEffectiveLun() const +{ + return GetLun(); +} diff --git a/cpp/controllers/sasi_controller.h b/cpp/controllers/sasi_controller.h new file mode 100644 index 00000000..21933e70 --- /dev/null +++ b/cpp/controllers/sasi_controller.h @@ -0,0 +1,32 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "generic_controller.h" + +class SasiController : public GenericController +{ + +public: + + SasiController(BUS& bus, int target_id, int lun_max) : GenericController(bus, target_id, lun_max) { } + ~SasiController() override = default; + + int GetEffectiveLun() const override; + + void MsgOut() override; + +private: + + bool XferMsg(int) override; + + void ParseMessage() override; + void ProcessMessage() override; + void ProcessExtendedMessage() override; +}; diff --git a/cpp/controllers/scsi_controller.cpp b/cpp/controllers/scsi_controller.cpp new file mode 100644 index 00000000..553366f7 --- /dev/null +++ b/cpp/controllers/scsi_controller.cpp @@ -0,0 +1,182 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +// Copyright (C) 2014-2020 GIMONS +// Copyright (C) akuker +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "base/primary_device.h" +#include "scsi_controller.h" + +void ScsiController::Reset() +{ + GenericController::Reset(); + + identified_lun = -1; + + scsi = {}; +} + +void ScsiController::BusFree() +{ + if (!IsBusFree()) { + // Initialize ATN message reception status + scsi.atnmsg = false; + + identified_lun = -1; + } + + GenericController::BusFree(); +} + +void ScsiController::MsgOut() +{ + if (!IsMsgOut()) { + // Process the IDENTIFY message + if (IsSelection()) { + scsi.atnmsg = true; + scsi.msc = 0; + scsi.msb = {}; + } + + LogTrace("Message Out phase"); + SetPhase(phase_t::msgout); + + GetBus().SetMSG(true); + GetBus().SetCD(true); + GetBus().SetIO(false); + + // Data transfer is 1 byte x 1 block + ResetOffset(); + SetLength(1); + SetBlocks(1); + + return; + } + + Receive(); +} + +bool ScsiController::XferMsg(int msg) +{ + assert(IsMsgOut()); + + // Save message out data + if (scsi.atnmsg) { + scsi.msb[scsi.msc] = (uint8_t)msg; + scsi.msc++; + scsi.msc %= 256; + } + + return true; +} + +void ScsiController::ParseMessage() +{ + int i = 0; + while (i < scsi.msc) { + const uint8_t message_type = scsi.msb[i]; + + if (message_type == 0x06) { + LogTrace("Received ABORT message"); + BusFree(); + return; + } + + if (message_type == 0x0C) { + LogTrace("Received BUS DEVICE RESET message"); + scsi.syncoffset = 0; + if (auto device = GetDeviceForLun(identified_lun); device) { + device->DiscardReservation(); + } + BusFree(); + return; + } + + if (message_type >= 0x80) { + identified_lun = static_cast(message_type) & 0x1F; + LogTrace(fmt::format("Received IDENTIFY message for LUN {}", identified_lun)); + } + + if (message_type == 0x01) { + LogTrace("Received EXTENDED MESSAGE"); + + // Check only when synchronous transfer is possible + // TODO Is this needed? + if (!scsi.syncenable || scsi.msb[i + 2] != 0x01) { + SetLength(1); + SetBlocks(1); + GetBuffer()[0] = 0x07; + MsgIn(); + return; + } + + scsi.syncperiod = scsi.msb[i + 3]; + if (scsi.syncperiod > MAX_SYNC_PERIOD) { + scsi.syncperiod = MAX_SYNC_PERIOD; + } + + scsi.syncoffset = scsi.msb[i + 4]; + if (scsi.syncoffset > MAX_SYNC_OFFSET) { + scsi.syncoffset = MAX_SYNC_OFFSET; + } + + // STDR response message generation + SetLength(5); + SetBlocks(1); + GetBuffer()[0] = 0x01; + GetBuffer()[1] = 0x03; + GetBuffer()[2] = 0x01; + GetBuffer()[3] = scsi.syncperiod; + GetBuffer()[4] = scsi.syncoffset; + MsgIn(); + return; + } + + // Next message + i++; + } +} + +void ScsiController::ProcessMessage() +{ + // Continue message out phase as long as ATN keeps asserting + if (GetBus().GetATN()) { + // Data transfer is 1 byte x 1 block + ResetOffset(); + SetLength(1); + SetBlocks(1); + return; + } + + if (scsi.atnmsg) { + ParseMessage(); + } + + // Initialize ATN message reception status + scsi.atnmsg = false; + + Command(); +} + +void ScsiController::ProcessExtendedMessage() +{ + // Completed sending response to extended message of IDENTIFY message + if (scsi.atnmsg) { + scsi.atnmsg = false; + + Command(); + } else { + BusFree(); + } +} + +int ScsiController::GetEffectiveLun() const +{ + // Return LUN from IDENTIFY message, or return the LUN from the CDB as fallback + return identified_lun != -1 ? identified_lun : GetLun(); +} diff --git a/cpp/controllers/scsi_controller.h b/cpp/controllers/scsi_controller.h new file mode 100644 index 00000000..ccf21c2b --- /dev/null +++ b/cpp/controllers/scsi_controller.h @@ -0,0 +1,70 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +// Copyright (C) 2014-2020 GIMONS +// Copyright (C) akuker +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "generic_controller.h" +#include + +using namespace std; + +class ScsiController : public GenericController +{ + // Transfer period factor (limited to 50 x 4 = 200ns) + static const int MAX_SYNC_PERIOD = 50; + + // REQ/ACK offset(limited to 16) + static const uint8_t MAX_SYNC_OFFSET = 16; + + // TODO Are data for synchronous transfers required at all? + // The Pi does not support this. Consider rejecting related messages and removing this structure. + using scsi_t = struct _scsi_t { + // Synchronous transfer possible + bool syncenable; + // Synchronous transfer period + uint8_t syncperiod = MAX_SYNC_PERIOD; + // Synchronous transfer offset + uint8_t syncoffset; + // Number of synchronous transfer ACKs + int syncack; + + bool atnmsg; + + int msc; + array msb; + }; + +public: + + ScsiController(BUS& bus, int target_id, int lun_max) : GenericController(bus, target_id, lun_max) { } + ~ScsiController() override = default; + + void Reset() override; + + int GetEffectiveLun() const override; + + void BusFree() override; + void MsgOut() override; + +private: + + bool XferMsg(int) override; + + void ParseMessage() override; + void ProcessMessage() override; + void ProcessExtendedMessage() override; + + // The LUN from the IDENTIFY message + int identified_lun = -1; + + scsi_t scsi = {}; +}; + diff --git a/cpp/devices/cd_dvd.cpp b/cpp/devices/cd_dvd.cpp new file mode 100644 index 00000000..0bfb1b3a --- /dev/null +++ b/cpp/devices/cd_dvd.cpp @@ -0,0 +1,364 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +// Copyright (C) 2014-2020 GIMONS +// Copyright (C) akuker +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/shared_exceptions.h" +#include "base/scsi_command_util.h" +#include "cd_dvd.h" +#include +#include + +using namespace scsi_defs; +using namespace scsi_command_util; + +CdDvd::CdDvd(int lun, scsi_defs::scsi_level level) : Disk(SCCD, lun, { 512, 2048 }), scsi_level(level) +{ + SetReadOnly(true); + SetRemovable(true); + SetLockable(true); +} + +bool CdDvd::Init(const param_map& params) +{ + Disk::Init(params); + + AddCommand(scsi_command::cmd_read_toc, [this] { ReadToc(); }); + + return true; +} + +void CdDvd::Open() +{ + assert(!IsReady()); + + // Initialization, track clear + SetBlockCount(0); + rawfile = false; + ClearTrack(); + + // Default sector size is 2048 bytes + SetSectorSizeInBytes(GetConfiguredSectorSize() ? GetConfiguredSectorSize() : 2048); + + // Judge whether it is a CUE sheet or an ISO file + array cue; + ifstream in(GetFilename(), ios::binary); + in.read(cue.data(), cue.size()); + if (!in.good()) { + throw io_exception("Can't read header of CD-ROM file '" + GetFilename() + "'"); + } + + // If it starts with FILE consider it a CUE sheet + if (!strncasecmp(cue.data(), "FILE", cue.size())) { + throw io_exception("CUE CD-ROM files are not supported"); + } else { + OpenIso(); + } + + Disk::ValidateFile(); + + SetUpCache(0, rawfile); + + SetReadOnly(true); + SetProtectable(false); + + if (IsReady()) { + SetAttn(true); + } +} + +void CdDvd::OpenIso() +{ + const off_t size = GetFileSize(); + if (size < 2048) { + throw io_exception("ISO CD-ROM file size must be at least 2048 bytes"); + } + + // Validate header + array header; + ifstream in(GetFilename(), ios::binary); + in.read(header.data(), header.size()); + if (!in.good()) { + throw io_exception("Can't read header of ISO CD-ROM file"); + } + + // Check if it is in RAW format + array sync = {}; + // 00,FFx10,00 is presumed to be RAW format + fill_n(sync.begin() + 1, 10, 0xff); + rawfile = false; + + if (memcmp(header.data(), sync.data(), sync.size()) == 0) { + // Supports MODE1/2048 or MODE1/2352 only + if (header[15] != 0x01) { + // Different mode + throw io_exception("Illegal raw ISO CD-ROM file header"); + } + + rawfile = true; + } + + if (rawfile) { + if (size % 2536) { + LogWarn("Raw ISO CD-ROM file size is not a multiple of 2536 bytes but is " + + to_string(size) + " bytes"); + } + + SetBlockCount(static_cast(size / 2352)); + } else { + SetBlockCount(static_cast(size >> GetSectorSizeShiftCount())); + } + + CreateDataTrack(); +} + +void CdDvd::CreateDataTrack() +{ + // Create only one data track + assert(!tracks.size()); + auto track = make_unique(); + track->Init(1, 0, static_cast(GetBlockCount()) - 1); + track->SetPath(false, GetFilename()); + tracks.push_back(std::move(track)); + dataindex = 0; +} + +void CdDvd::ReadToc() +{ + GetController()->SetLength(ReadTocInternal(GetController()->GetCmd(), GetController()->GetBuffer())); + + EnterDataInPhase(); +} + +vector CdDvd::InquiryInternal() const +{ + return HandleInquiry(device_type::cd_rom, scsi_level, true); +} + +void CdDvd::SetUpModePages(map>& pages, int page, bool changeable) const +{ + Disk::SetUpModePages(pages, page, changeable); + + if (page == 0x0d || page == 0x3f) { + AddCDROMPage(pages, changeable); + } + + if (page == 0x0e || page == 0x3f) { + AddCDDAPage(pages, changeable); + } +} + +void CdDvd::AddCDROMPage(map>& pages, bool changeable) const +{ + vector buf(8); + + // No changeable area + if (!changeable) { + // 2 seconds for inactive timer + buf[3] = (byte)0x05; + + // MSF multiples are 60 and 75 respectively + buf[5] = (byte)60; + buf[7] = (byte)75; + } + + pages[13] = buf; +} + +void CdDvd::AddCDDAPage(map>& pages, bool) const +{ + vector buf(16); + + // Audio waits for operation completion and allows + // PLAY across multiple tracks + + pages[14] = buf; +} + +void CdDvd::AddVendorPage(map>& pages, int page, bool changeable) const +{ + // Page code 48 + if (page == 0x30 || page == 0x3f) { + AddAppleVendorModePage(pages, changeable); + } +} + +int CdDvd::Read(span buf, uint64_t block) +{ + CheckReady(); + + const int index = SearchTrack(static_cast(block)); + if (index < 0) { + throw scsi_exception(sense_key::illegal_request, asc::lba_out_of_range); + } + + assert(tracks[index]); + + // If different from the current data track + if (dataindex != index) { + // Reset the number of blocks + SetBlockCount(tracks[index]->GetBlocks()); + assert(GetBlockCount() > 0); + + // Re-assign disk cache (no need to save) + Resize_cache(tracks[index]->GetPath(), rawfile); + + // Reset data index + dataindex = index; + } + + assert(dataindex >= 0); + return Disk::Read(buf, block); +} + +int CdDvd::ReadTocInternal(cdb_t cdb, vector& buf) +{ + CheckReady(); + + // If ready, there is at least one track + assert(!tracks.empty()); + assert(tracks[0]); + + // Get allocation length, clear buffer + const int length = GetInt16(cdb, 7); + fill_n(buf.data(), length, 0); + + // Get MSF Flag + const bool msf = cdb[1] & 0x02; + + // Get and check the last track number + const int last = tracks[tracks.size() - 1]->GetTrackNo(); + // Except for AA + if (cdb[6] > last && cdb[6] != 0xaa) { + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } + + // Check start index + int index = 0; + if (cdb[6] != 0x00) { + // Advance the track until the track numbers match + while (tracks[index]) { + if (cdb[6] == tracks[index]->GetTrackNo()) { + break; + } + index++; + } + + // AA if not found or internal error + if (!tracks[index]) { + if (cdb[6] != 0xaa) { + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } + + // Returns the final LBA+1 because it is AA + buf[0] = 0x00; + buf[1] = 0x0a; + buf[2] = (uint8_t)tracks[0]->GetTrackNo(); + buf[3] = (uint8_t)last; + buf[6] = 0xaa; + const uint32_t lba = tracks[tracks.size() - 1]->GetLast() + 1; + if (msf) { + LBAtoMSF(lba, &buf[8]); + } else { + SetInt16(buf, 10, lba); + } + + return length; + } + } + + // Number of track descriptors returned this time (number of loops) + const int loop = last - tracks[index]->GetTrackNo() + 1; + assert(loop >= 1); + + // Create header + SetInt16(buf, 0, (loop << 3) + 2); + buf[2] = (uint8_t)tracks[0]->GetTrackNo(); + buf[3] = (uint8_t)last; + + int offset = 4; + + for (int i = 0; i < loop; i++) { + // ADR and Control + if (tracks[index]->IsAudio()) { + // audio track + buf[offset + 1] = 0x10; + } else { + // data track + buf[offset + 1] = 0x14; + } + + // track number + buf[offset + 2] = (uint8_t)tracks[index]->GetTrackNo(); + + // track address + if (msf) { + LBAtoMSF(tracks[index]->GetFirst(), &buf[offset + 4]); + } else { + SetInt16(buf, offset + 6, tracks[index]->GetFirst()); + } + + // Advance buffer pointer and index + offset += 8; + index++; + } + + // Always return only the allocation length + return length; +} + +void CdDvd::LBAtoMSF(uint32_t lba, uint8_t *msf) const +{ + // 75 and 75*60 get the remainder + uint32_t m = lba / (75 * 60); + uint32_t s = lba % (75 * 60); + const uint32_t f = s % 75; + s /= 75; + + // The base point is M=0, S=2, F=0 + s += 2; + if (s >= 60) { + s -= 60; + m++; + } + + // Store + assert(m < 0x100); + assert(s < 60); + assert(f < 75); + msf[0] = 0x00; + msf[1] = (uint8_t)m; + msf[2] = (uint8_t)s; + msf[3] = (uint8_t)f; +} + +void CdDvd::ClearTrack() +{ + tracks.clear(); + + // No settings for data and audio + dataindex = -1; + audioindex = -1; +} + +int CdDvd::SearchTrack(uint32_t lba) const +{ + // Track loop + for (size_t i = 0; i < tracks.size(); i++) { + // Listen to the track + assert(tracks[i]); + if (tracks[i]->IsValid(lba)) { + return static_cast(i); + } + } + + // Track wasn't found + return -1; +} + diff --git a/cpp/devices/cd_dvd.h b/cpp/devices/cd_dvd.h new file mode 100644 index 00000000..d5c29867 --- /dev/null +++ b/cpp/devices/cd_dvd.h @@ -0,0 +1,64 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +// Copyright (C) 2014-2020 GIMONS +// Copyright (C) akuker +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "cd_track.h" +#include "disk.h" +#include "base/interfaces/scsi_mmc_commands.h" +#include +#include +#include + +class CdDvd : public Disk, private ScsiMmcCommands +{ +public: + + explicit CdDvd(int, scsi_defs::scsi_level = scsi_level::scsi_2); + ~CdDvd() override = default; + + bool Init(const param_map&) override; + + void Open() override; + + vector InquiryInternal() const override; + int Read(span, uint64_t) override; + +protected: + + void SetUpModePages(map>&, int, bool) const override; + void AddVendorPage(map>&, int, bool) const override; + +private: + + int ReadTocInternal(cdb_t, vector&); + + void AddCDROMPage(map>&, bool) const; + void AddCDDAPage(map>&, bool) const; + scsi_defs::scsi_level scsi_level; + + void OpenIso(); + + void CreateDataTrack(); + + void ReadToc() override; + + void LBAtoMSF(uint32_t, uint8_t *) const; // LBA→MSF conversion + + bool rawfile = false; // RAW flag + + // Track management + void ClearTrack(); // Clear the track + int SearchTrack(uint32_t lba) const; // Track search + vector> tracks; // Track opbject references + int dataindex = -1; // Current data track + int audioindex = -1; // Current audio track +}; diff --git a/cpp/devices/cd_track.cpp b/cpp/devices/cd_track.cpp new file mode 100644 index 00000000..de8cb72c --- /dev/null +++ b/cpp/devices/cd_track.cpp @@ -0,0 +1,122 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +// Copyright (C) 2014-2020 GIMONS +// Copyright (C) akuker +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "cd_track.h" +#include + +void CDTrack::Init(int track, uint32_t first, uint32_t last) +{ + assert(!valid); + assert(track >= 1); + assert(first < last); + + // Set and enable track number + track_no = track; + valid = true; + + // Remember LBA + first_lba = first; + last_lba = last; +} + +void CDTrack::SetPath(bool cdda, string_view path) +{ + assert(valid); + + // CD-DA or data + audio = cdda; + + // Remember the path + imgpath = path; +} + +string CDTrack::GetPath() const +{ + assert(valid); + + return imgpath; +} + +//--------------------------------------------------------------------------- +// +// Gets the start of LBA +// +//--------------------------------------------------------------------------- +uint32_t CDTrack::GetFirst() const +{ + assert(valid); + assert(first_lba < last_lba); + + return first_lba; +} + +//--------------------------------------------------------------------------- +// +// Get the end of LBA +// +//--------------------------------------------------------------------------- +uint32_t CDTrack::GetLast() const +{ + assert(valid); + assert(first_lba < last_lba); + + return last_lba; +} + +uint32_t CDTrack::GetBlocks() const +{ + assert(valid); + assert(first_lba < last_lba); + + // Calculate from start LBA and end LBA + return last_lba - first_lba + 1; +} + +int CDTrack::GetTrackNo() const +{ + assert(valid); + assert(track_no >= 1); + + return track_no; +} + +//--------------------------------------------------------------------------- +// +// Is valid block +// +//--------------------------------------------------------------------------- +bool CDTrack::IsValid(uint32_t lba) const +{ + // false if the track itself is invalid + if (!valid) { + return false; + } + + // If the block is BEFORE the first block + if (lba < first_lba) { + return false; + } + + // If the block is AFTER the last block + if (last_lba < lba) { + return false; + } + + // This track is valid + return true; +} + +bool CDTrack::IsAudio() const +{ + assert(valid); + + return audio; +} diff --git a/cpp/devices/cd_track.h b/cpp/devices/cd_track.h new file mode 100644 index 00000000..129d1378 --- /dev/null +++ b/cpp/devices/cd_track.h @@ -0,0 +1,46 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +// Copyright (C) 2014-2020 GIMONS +// Copyright (C) akuker +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include +#include + +using namespace std; + +class CDTrack final +{ +public: + + CDTrack() = default; + ~CDTrack() = default; + + void Init(int track, uint32_t first, uint32_t last); + + // Properties + void SetPath(bool, string_view); // Set the path + string GetPath() const; // Get the path + uint32_t GetFirst() const; // Get the start LBA + uint32_t GetLast() const; // Get the last LBA + uint32_t GetBlocks() const; // Get the number of blocks + int GetTrackNo() const; // Get the track number + bool IsValid(uint32_t lba) const; // Is this a valid LBA? + bool IsAudio() const; // Is this an audio track? + +private: + bool valid = false; // Valid track + int track_no = -1; // Track number + uint32_t first_lba = 0; // First LBA + uint32_t last_lba = 0; // Last LBA + bool audio = false; // Audio track flag + + string imgpath; // Image file path +}; diff --git a/cpp/devices/ctapdriver.cpp b/cpp/devices/ctapdriver.cpp new file mode 100644 index 00000000..a85e699e --- /dev/null +++ b/cpp/devices/ctapdriver.cpp @@ -0,0 +1,377 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Powered by XM6 TypeG Technology. +// Copyright (C) 2016-2020 GIMONS +// Copyright (C) akuker +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/s2p_util.h" +#include "shared/network_util.h" +#include "ctapdriver.h" +#include +#include +#include +#include +#include +#include +#include +#ifdef __linux__ +#include +#include +#endif + +using namespace std; +using namespace s2p_util; +using namespace network_util; + +const string CTapDriver::BRIDGE_NAME = "piscsi_bridge"; + +static string br_setif(int br_socket_fd, const string& bridgename, const string& ifname, bool add) { +#ifndef __linux__ + return "if_nametoindex: Linux is required"; +#else + ifreq ifr; + ifr.ifr_ifindex = if_nametoindex(ifname.c_str()); + if (ifr.ifr_ifindex == 0) { + return "Can't if_nametoindex " + ifname; + } + strncpy(ifr.ifr_name, bridgename.c_str(), IFNAMSIZ - 1); //NOSONAR Using strncpy is safe + if (ioctl(br_socket_fd, add ? SIOCBRADDIF : SIOCBRDELIF, &ifr) < 0) { + return "Can't ioctl " + string(add ? "SIOCBRADDIF" : "SIOCBRDELIF"); + } + return ""; +#endif +} + +string ip_link(int fd, const char* ifname, bool up) { + ifreq ifr; + strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1); //NOSONAR Using strncpy is safe + if (ioctl(fd, SIOCGIFFLAGS, &ifr)) { + return "Can't ioctl SIOCGIFFLAGS"; + } + ifr.ifr_flags &= ~IFF_UP; + if (up) { + ifr.ifr_flags |= IFF_UP; + } + if (ioctl(fd, SIOCSIFFLAGS, &ifr)) { + return "Can't ioctl SIOCSIFFLAGS"; + } + return ""; +} + +bool CTapDriver::Init(const param_map& const_params) +{ + param_map params = const_params; + stringstream s(params["interface"]); + string interface; + while (getline(s, interface, ',')) { + interfaces.push_back(interface); + } + inet = params["inet"]; + + spdlog::trace("Opening tap device"); + // TAP device initilization + if ((m_hTAP = open("/dev/net/tun", O_RDWR)) < 0) { + LogErrno("Can't open tun"); + return false; + } + +#ifndef __linux__ + return false; +#else + + // IFF_NO_PI for no extra packet information + ifreq ifr = {}; + ifr.ifr_flags = IFF_TAP | IFF_NO_PI; + strncpy(ifr.ifr_name, "piscsi0", IFNAMSIZ - 1); //NOSONAR Using strncpy is safe + + spdlog::trace("Going to open " + string(ifr.ifr_name)); + + const int ret = ioctl(m_hTAP, TUNSETIFF, (void *)&ifr); + if (ret < 0) { + LogErrno("Can't ioctl TUNSETIFF"); + + close(m_hTAP); + return false; + } + + spdlog::trace("Return code from ioctl was " + to_string(ret)); + + const int ip_fd = socket(PF_INET, SOCK_DGRAM, 0); + if (ip_fd < 0) { + LogErrno("Can't open ip socket"); + + close(m_hTAP); + return false; + } + + const int br_socket_fd = socket(AF_LOCAL, SOCK_STREAM, 0); + if (br_socket_fd < 0) { + LogErrno("Can't open bridge socket"); + + close(m_hTAP); + close(ip_fd); + return false; + } + + auto cleanUp = [&] (const string& error) { + LogErrno(error); + close(m_hTAP); + close(ip_fd); + close(br_socket_fd); + return false; + }; + + // Check if the bridge has already been created + // TODO Find an alternative to accessing a file, there is most likely a system call/ioctl + if (access(string("/sys/class/net/" + BRIDGE_NAME).c_str(), F_OK)) { + spdlog::trace("Checking which interface is available for creating the bridge " + BRIDGE_NAME); + + const auto& it = ranges::find_if(interfaces, [] (const string& iface) { return IsInterfaceUp(iface); } ); + if (it == interfaces.end()) { + return cleanUp("No interface is up, not creating bridge " + BRIDGE_NAME); + } + + const string bridge_interface = *it; + + spdlog::info("Creating " + BRIDGE_NAME + " for interface " + bridge_interface); + + if (bridge_interface == "eth0") { + if (const string error = SetUpEth0(br_socket_fd, bridge_interface); !error.empty()) { + return cleanUp(error); + } + } + else if (const string error = SetUpNonEth0(br_socket_fd, ip_fd, inet); !error.empty()) { + return cleanUp(error); + } + + spdlog::trace(">ip link set dev " + BRIDGE_NAME + " up"); + + if (const string error = ip_link(ip_fd, BRIDGE_NAME.c_str(), true); !error.empty()) { + return cleanUp(error); + } + } + else { + spdlog::info(BRIDGE_NAME + " is already available"); + } + + spdlog::trace(">ip link set piscsi0 up"); + + if (const string error = ip_link(ip_fd, "piscsi0", true); !error.empty()) { + return cleanUp(error); + } + + spdlog::trace(">brctl addif " + BRIDGE_NAME + " piscsi0"); + + if (const string error = br_setif(br_socket_fd, BRIDGE_NAME, "piscsi0", true); !error.empty()) { + return cleanUp(error); + } + + close(ip_fd); + close(br_socket_fd); + + spdlog::info("Tap device " + string(ifr.ifr_name) + " created"); + + return true; +#endif +} + +void CTapDriver::CleanUp() const +{ + if (m_hTAP != -1) { + if (const int br_socket_fd = socket(AF_LOCAL, SOCK_STREAM, 0); br_socket_fd < 0) { + LogErrno("Can't open bridge socket"); + } else { + spdlog::trace(">brctl delif " + BRIDGE_NAME + " piscsi0"); + if (const string error = br_setif(br_socket_fd, BRIDGE_NAME, "piscsi0", false); !error.empty()) { + spdlog::warn("Warning: Removing piscsi0 from the bridge failed: " + error); + spdlog::warn("You may need to manually remove the tap device"); + } + close(br_socket_fd); + } + + // Release TAP device + close(m_hTAP); + } +} + +param_map CTapDriver::GetDefaultParams() const +{ + return { + { "interface", Join(GetNetworkInterfaces(), ",") }, + { "inet", DEFAULT_IP } + }; +} + +pair CTapDriver::ExtractAddressAndMask(const string& s) +{ + string address = s; + string netmask = "255.255.255.0"; //NOSONAR This hardcoded IP address is safe + if (const auto& components = Split(s, '/', 2); components.size() == 2) { + address = components[0]; + + int m; + if (!GetAsUnsignedInt(components[1], m) || m < 8 || m > 32) { + spdlog::error("Invalid CIDR netmask notation '" + components[1] + "'"); + return { "", "" }; + } + + // long long is required for compatibility with 32 bit platforms + const auto mask = (long long)(pow(2, 32) - (1 << (32 - m))); + netmask = to_string((mask >> 24) & 0xff) + '.' + to_string((mask >> 16) & 0xff) + '.' + + to_string((mask >> 8) & 0xff) + '.' + to_string(mask & 0xff); + } + + return { address, netmask }; +} + +string CTapDriver::SetUpEth0(int socket_fd, const string& bridge_interface) +{ +#ifdef __linux__ + spdlog::trace(">brctl addbr " + BRIDGE_NAME); + + if (ioctl(socket_fd, SIOCBRADDBR, BRIDGE_NAME.c_str()) < 0) { + return "Can't ioctl SIOCBRADDBR"; + } + + spdlog::trace(">brctl addif " + BRIDGE_NAME + " " + bridge_interface); + + if (const string error = br_setif(socket_fd, BRIDGE_NAME, bridge_interface, true); !error.empty()) { + return error; + } +#endif + + return ""; +} + +string CTapDriver::SetUpNonEth0(int socket_fd, int ip_fd, const string& s) +{ + const auto [address, netmask] = ExtractAddressAndMask(s); + if (address.empty() || netmask.empty()) { + return "Error extracting inet address and netmask"; + } + +#ifdef __linux__ + spdlog::trace(">brctl addbr " + BRIDGE_NAME); + + if (ioctl(socket_fd, SIOCBRADDBR, BRIDGE_NAME.c_str()) < 0) { + return "Can't ioctl SIOCBRADDBR"; + } + + ifreq ifr_a; + ifr_a.ifr_addr.sa_family = AF_INET; + strncpy(ifr_a.ifr_name, BRIDGE_NAME.c_str(), IFNAMSIZ - 1); //NOSONAR Using strncpy is safe + if (auto addr = (sockaddr_in*)&ifr_a.ifr_addr; + inet_pton(AF_INET, address.c_str(), &addr->sin_addr) != 1) { + return "Can't convert '" + address + "' into a network address"; + } + + ifreq ifr_n; + ifr_n.ifr_addr.sa_family = AF_INET; + strncpy(ifr_n.ifr_name, BRIDGE_NAME.c_str(), IFNAMSIZ - 1); //NOSONAR Using strncpy is safe + if (auto mask = (sockaddr_in*)&ifr_n.ifr_addr; + inet_pton(AF_INET, netmask.c_str(), &mask->sin_addr) != 1) { + return "Can't convert '" + netmask + "' into a netmask"; + } + + spdlog::trace(">ip address add " + s + " dev " + BRIDGE_NAME); + + if (ioctl(ip_fd, SIOCSIFADDR, &ifr_a) < 0 || ioctl(ip_fd, SIOCSIFNETMASK, &ifr_n) < 0) { + return "Can't ioctl SIOCSIFADDR or SIOCSIFNETMASK"; + } +#endif + + return ""; +} + +string CTapDriver::IpLink(bool enable) const +{ + const int fd = socket(PF_INET, SOCK_DGRAM, 0); + spdlog::trace(string(">ip link set piscsi0 ") + (enable ? "up" : "down")); + const string result = ip_link(fd, "piscsi0", enable); + close(fd); + return result; +} + +void CTapDriver::Flush() const +{ + while (HasPendingPackets()) { + array m_garbage_buffer; + (void)Receive(m_garbage_buffer.data()); + } +} + +bool CTapDriver::HasPendingPackets() const +{ + assert(m_hTAP != -1); + + // Check if there is data that can be received + pollfd fds; + fds.fd = m_hTAP; + fds.events = POLLIN | POLLERR; + fds.revents = 0; + poll(&fds, 1, 0); + spdlog::trace(to_string(fds.revents) + " revents"); + return fds.revents & POLLIN; +} + +// See https://stackoverflow.com/questions/21001659/crc32-algorithm-implementation-in-c-without-a-look-up-table-and-with-a-public-li +uint32_t CTapDriver::Crc32(span data) { + uint32_t crc = 0xffffffff; + for (const auto d: data) { + crc ^= d; + for (int i = 0; i < 8; i++) { + const uint32_t mask = -(static_cast(crc) & 1); + crc = (crc >> 1) ^ (0xEDB88320 & mask); + } + } + return ~crc; +} + +int CTapDriver::Receive(uint8_t *buf) const +{ + assert(m_hTAP != -1); + + // Check if there is data that can be received + if (!HasPendingPackets()) { + return 0; + } + + // Receive + auto dwReceived = static_cast(read(m_hTAP, buf, ETH_FRAME_LEN)); + if (dwReceived == static_cast(-1)) { + spdlog::warn("Error occured while receiving a packet"); + return 0; + } + + // If reception is enabled + if (dwReceived > 0) { + // We need to add the Frame Check Status (FCS) CRC back onto the end of the packet. + // The Linux network subsystem removes it, since most software apps shouldn't ever + // need it. + const int crc = Crc32(span(buf, dwReceived)); + + buf[dwReceived + 0] = (uint8_t)((crc >> 0) & 0xFF); + buf[dwReceived + 1] = (uint8_t)((crc >> 8) & 0xFF); + buf[dwReceived + 2] = (uint8_t)((crc >> 16) & 0xFF); + buf[dwReceived + 3] = (uint8_t)((crc >> 24) & 0xFF); + + // Add FCS size to the received message size + dwReceived += 4; + } + + // Return the number of bytes + return dwReceived; +} + +int CTapDriver::Send(const uint8_t *buf, int len) const +{ + assert(m_hTAP != -1); + + // Start sending + return static_cast(write(m_hTAP, buf, len)); +} diff --git a/cpp/devices/ctapdriver.h b/cpp/devices/ctapdriver.h new file mode 100644 index 00000000..43e50d5f --- /dev/null +++ b/cpp/devices/ctapdriver.h @@ -0,0 +1,71 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// for Raspberry Pi +// +// Powered by XM6 TypeG Technology. +// Copyright (C) 2016-2020 GIMONS +// Copyright (C) akuker +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "base/device.h" +#include +#include +#include + +#ifndef ETH_FRAME_LEN +static const int ETH_FRAME_LEN = 1514; +#endif +#ifndef ETH_FCS_LEN +static const int ETH_FCS_LEN = 4; +#endif + +using namespace std; + +class CTapDriver +{ + static const string BRIDGE_NAME; + + const inline static string DEFAULT_IP = "10.10.20.1/24"; //NOSONAR This hardcoded IP address is safe + +public: + + CTapDriver() = default; + ~CTapDriver() = default; + CTapDriver(CTapDriver&) = default; + CTapDriver& operator=(const CTapDriver&) = default; + + bool Init(const param_map&); + void CleanUp() const; + + param_map GetDefaultParams() const; + + int Receive(uint8_t *) const; + int Send(const uint8_t *, int) const; + bool HasPendingPackets() const; + // Enable/Disable the piscsi0 interface + string IpLink(bool) const; + // Purge all of the packets that are waiting to be processed + void Flush() const; + + static uint32_t Crc32(span); + +private: + + static string SetUpEth0(int, const string&); + static string SetUpNonEth0(int, int, const string&); + static pair ExtractAddressAndMask(const string&); + + // File handle + int m_hTAP = -1; + + // Prioritized comma-separated list of interfaces to create the bridge for + vector interfaces; + + string inet; +}; + diff --git a/cpp/devices/daynaport.cpp b/cpp/devices/daynaport.cpp new file mode 100644 index 00000000..29911fe7 --- /dev/null +++ b/cpp/devices/daynaport.cpp @@ -0,0 +1,431 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2014-2020 GIMONS +// Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +// Copyright (C) 2020 akuker +// Copyright (C) 2023 Uwe Seimet +// +// This design is derived from the SLINKCMD.TXT file, as well as David Kuder's +// Tiny SCSI Emulator +// - SLINKCMD: http://www.bitsavers.org/pdf/apple/scsi/dayna/daynaPORT/SLINKCMD.TXT +// - Tiny SCSI : https://hackaday.io/project/18974-tiny-scsi-emulator +// +// Additional documentation and clarification is available at the +// following link: +// - https://github.com/PiSCSI/piscsi/wiki/Dayna-Port-SCSI-Link +// +// Note: This requires a DaynaPort SCSI Link driver. +// +//--------------------------------------------------------------------------- + +#include "shared/shared_exceptions.h" +#include "base/scsi_command_util.h" +#include "daynaport.h" + +using namespace scsi_defs; +using namespace scsi_command_util; + +SCSIDaynaPort::SCSIDaynaPort(int lun) : PrimaryDevice(SCDP, lun) +{ + SupportsParams(true); +} + +bool SCSIDaynaPort::Init(const param_map& params) +{ + PrimaryDevice::Init(params); + + AddCommand(scsi_command::cmd_test_unit_ready, [this] { TestUnitReady(); }); + AddCommand(scsi_command::cmd_read6, [this] { Read6(); }); + AddCommand(scsi_command::cmd_write6, [this] { Write6(); }); + AddCommand(scsi_command::cmd_retrieve_stats, [this] { RetrieveStatistics(); }); + AddCommand(scsi_command::cmd_set_iface_mode, [this] { SetInterfaceMode(); }); + AddCommand(scsi_command::cmd_set_mcast_addr, [this] { SetMcastAddr(); }); + AddCommand(scsi_command::cmd_enable_interface, [this] { EnableInterface(); }); + + // The MacOS Daynaport driver needs to have a delay after the size/flags field of the read response + SetSendDelay(DAYNAPORT_READ_HEADER_SZ); + + tap_enabled = tap.Init(GetParams()); + if (!tap_enabled) { +// Not terminating on a regular PC is helpful for testing +#if !defined(__x86_64__) && !defined(__X86__) + return false; +#endif + } else { + LogTrace("Tap interface created"); + } + + Reset(); + SetReady(true); + SetReset(false); + + return true; +} + +void SCSIDaynaPort::CleanUp() +{ + tap.CleanUp(); +} + +vector SCSIDaynaPort::InquiryInternal() const +{ + vector buf = HandleInquiry(device_type::processor, scsi_level::scsi_2, false); + + // The Daynaport driver for the Mac expects 37 bytes: Increase additional length and + // add a vendor-specific byte in order to satisfy this driver. + buf[4]++; + buf.push_back(0); + + return buf; +} + +//--------------------------------------------------------------------------- +// +// READ +// +// Command: 08 00 00 LL LL XX (LLLL is data length, XX = c0 or 80) +// Function: Read a packet at a time from the device (standard SCSI Read) +// Type: Input; the following data is returned: +// LL LL NN NN NN NN XX XX XX ... CC CC CC CC +// where: +// LLLL is normally the length of the packet (a 2-byte +// big-endian hex value), including 4 trailing bytes +// of CRC, but excluding itself and the flag field. +// See below for special values +// NNNNNNNN is a 4-byte flag field with the following meanings: +// FFFFFFFF a packet has been dropped (?); in this case +// the length field appears to be always 4000 +// 00000010 there are more packets currently available +// in SCSI/Link memory +// 00000000 this is the last packet +// XX XX ... is the actual packet +// CCCCCCCC is the CRC +// +// Notes: +// - When all packets have been retrieved successfully, a length field +// of 0000 is returned; however, if a packet has been dropped, the +// SCSI/Link will instead return a non-zero length field with a flag +// of FFFFFFFF when there are no more packets available. This behaviour +// seems to continue until a disable/enable sequence has been issued. +// - The SCSI/Link apparently has about 6KB buffer space for packets. +// +//--------------------------------------------------------------------------- +int SCSIDaynaPort::Read(cdb_t cdb, vector& buf, uint64_t) +{ + const auto response = (scsi_resp_read_t*)buf.data(); + + const int requested_length = cdb[4]; + + LogTrace(fmt::format("Read maximum length: {}", requested_length)); + + // At startup the host may send a READ(6) command with a sector count of 1 to read the root sector. + // We should respond by going into the status mode with a code of 0x02. + if (requested_length == 1) { + return 0; + } + + // Some of the packets we receive will not be for us. So, we'll keep pulling messages + // until the buffer is empty, or we've read X times. (X is just a made up number) + int read_count = 0; + while (read_count < MAX_READ_RETRIES) { + read_count++; + + // The first 2 bytes are reserved for the length of the packet + // The next 4 bytes are reserved for a flag field + const int rx_packet_size = tap.Receive(&buf[DAYNAPORT_READ_HEADER_SZ]); + + // If we didn't receive anything, return size of 0 + if (rx_packet_size <= 0) { + LogTrace("No packet received"); + response->length = 0; + response->flags = read_data_flags_t::e_no_more_data; + return DAYNAPORT_READ_HEADER_SZ; + } + + byte_read_count += rx_packet_size; + + LogTrace(fmt::format("Packet Size {0}, read count: {1}", rx_packet_size, read_count)); + + // TODO: We should check to see if this message is in the multicast + // configuration from SCSI command 0x0D + + int size = rx_packet_size; + if (size < 128) { + // A frame must have at least 64 bytes for the Atari driver, see https://github.com/PiSCSI/piscsi/issues/619, + // but also works with 128 bytes. + // The NetBSD driver requires at least 128 bytes, see https://github.com/PiSCSI/piscsi/issues/1098. + // The Mac driver is also fine with 128 bytes. + // Note that this work-around breaks the checksum. As currently there are no known drivers + // that care for the checksum it was decided to accept the broken checksum. + // If a driver should pop up that breaks because of this, the work-around has to be re-evaluated. + size = 128; + } + SetInt16(buf, 0, size); + SetInt32(buf, 2, tap.HasPendingPackets() ? 0x10 : 0x00); + + // Return the packet size + 2 for the length + 4 for the flag field + // The CRC was already appended by the ctapdriver + return size + DAYNAPORT_READ_HEADER_SZ; + + // If we got to this point, there are still messages in the queue, so + // we should loop back and get the next one. + } + + response->length = 0; + response->flags = read_data_flags_t::e_no_more_data; + return DAYNAPORT_READ_HEADER_SZ; +} + +//--------------------------------------------------------------------------- +// +// Write +// +// Command: 0a 00 00 LL LL XX (LLLL is data length, XX = 80 or 00) +// Function: Write a packet at a time to the device (standard SCSI Write) +// Type: Output; the format of the data to be sent depends on the value +// of XX, as follows: +// - if XX = 00, LLLL is the packet length, and the data to be sent +// must be an image of the data packet +// - if XX = 80, LLLL is the packet length + 8, and the data to be +// sent is: +// PP PP 00 00 XX XX XX ... 00 00 00 00 +// where: +// PPPP is the actual (2-byte big-endian) packet length +// XX XX ... is the actual packet +// +//--------------------------------------------------------------------------- +bool SCSIDaynaPort::Write(cdb_t cdb, span buf) +{ + if (const int data_format = cdb[5]; data_format == 0x00) { + const int data_length = GetInt16(cdb, 3); + tap.Send(buf.data(), data_length); + byte_write_count += data_length; + LogTrace(fmt::format("Transmitted {} byte(s) (00 format)", data_length)); + } + else if (data_format == 0x80) { + // The data length is specified in the first 2 bytes of the payload + const int data_length = buf[1] + ((static_cast(buf[0]) & 0xff) << 8); + tap.Send(&(buf.data()[4]), data_length); + byte_write_count += data_length; + LogTrace(fmt::format("Transmitted {} byte(s) (80 format)", data_length)); + } + else { + LogWarn(fmt::format("Unknown data format: ${}", data_format)); + } + + GetController()->SetBlocks(0); + + return true; +} + +//--------------------------------------------------------------------------- +// +// RetrieveStats +// +// Command: 09 00 00 00 12 00 +// Function: Retrieve MAC address and device statistics +// Type: Input; returns 18 (decimal) bytes of data as follows: +// - bytes 0-5: the current hardware ethernet (MAC) address +// - bytes 6-17: three long word (4-byte) counters (little-endian). +// Notes: The contents of the three longs are typically zero, and their +// usage is unclear; they are suspected to be: +// - long #1: frame alignment errors +// - long #2: CRC errors +// - long #3: frames lost +// +//--------------------------------------------------------------------------- +int SCSIDaynaPort::RetrieveStats(cdb_t cdb, vector& buf) const +{ + memcpy(buf.data(), &m_scsi_link_stats, sizeof(m_scsi_link_stats)); + + return static_cast(min(sizeof(m_scsi_link_stats), static_cast(GetInt16(cdb, 3)))); +} + +void SCSIDaynaPort::TestUnitReady() +{ + // Always successful + EnterStatusPhase(); +} + +void SCSIDaynaPort::Read6() +{ + // Get record number and block number + const uint32_t record = GetInt24(GetController()->GetCmd(), 1) & 0x1fffff; + GetController()->SetBlocks(1); + + // If any commands have a bogus control value, they were probably not + // generated by the DaynaPort driver so ignore them + if (GetController()->GetCmdByte(5) != 0xc0 && GetController()->GetCmdByte(5) != 0x80) { + LogTrace("Control value: " + to_string(GetController()->GetCmdByte(5))); + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } + + LogTrace(fmt::format("READ(6) command, record: ${:x}", record)); + + const int length = Read(GetController()->GetCmd(), GetController()->GetBuffer(), record); + LogTrace(fmt::format("Length is {}", GetController()->GetLength())); + GetController()->SetLength(length); + + // Set next block + GetController()->SetNext(record + 1); + + EnterDataInPhase(); +} + +void SCSIDaynaPort::Write6() const +{ + const int data_format = GetController()->GetCmdByte(5); + + int length = 0; + if (data_format == 0x00) { + length = GetInt16(GetController()->GetCmd(), 3); + } + else if (data_format == 0x80) { + length = GetInt16(GetController()->GetCmd(), 3) + 8; + } + + if (length <= 0) { + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } + + LogTrace(fmt::format("Length: {0}, format: ${1:02x}", GetController()->GetLength(), data_format)); + + GetController()->SetLength(length); + + // Set next block + GetController()->SetBlocks(1); + GetController()->SetNext(1); + + EnterDataOutPhase(); +} + +void SCSIDaynaPort::RetrieveStatistics() const +{ + GetController()->SetLength(RetrieveStats(GetController()->GetCmd(), GetController()->GetBuffer())); + + // Set next block + GetController()->SetBlocks(1); + GetController()->SetNext(1); + + EnterDataInPhase(); +} + +//--------------------------------------------------------------------------- +// +// Set interface mode/Set MAC address +// +// Set Interface Mode (0c) +// ----------------------- +// Command: 0c 00 00 00 FF 80 (FF = 08 or 04) +// Function: Allow interface to receive broadcast messages (FF = 04); the +// function of (FF = 08) is currently unknown. +// Type: No data transferred +// Notes: This command is accepted by firmware 1.4a & 2.0f, but has no +// effect on 2.0f, which is always capable of receiving broadcast +// messages. In 1.4a, once broadcast mode is set, it remains set +// until the interface is disabled. +// +// Set MAC Address (0c) +// -------------------- +// Command: 0c 00 00 00 FF 40 (FF = 08 or 04) +// Function: Set MAC address +// Type: Output; overrides built-in MAC address with user-specified +// 6-byte value +// Notes: This command is intended primarily for debugging/test purposes. +// Disabling the interface resets the MAC address to the built-in +// value. +// +//--------------------------------------------------------------------------- +void SCSIDaynaPort::SetInterfaceMode() const +{ + switch(GetController()->GetCmdByte(5)){ + case CMD_SCSILINK_SETMODE: + // Not implemented, do nothing + EnterStatusPhase(); + break; + + case CMD_SCSILINK_SETMAC: + GetController()->SetLength(6); + EnterDataOutPhase(); + break; + + default: + LogWarn(fmt::format("Unsupported SetInterface command: ${}", GetController()->GetCmdByte(5))); + throw scsi_exception(sense_key::illegal_request, asc::invalid_command_operation_code); + break; + } +} + +void SCSIDaynaPort::SetMcastAddr() const +{ + const int length = GetController()->GetCmdByte(4); + if (!length) { + LogWarn(fmt::format("Unsupported SetMcastAddr command: ${}", GetController()->GetCmdByte(2))); + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } + + GetController()->SetLength(length); + + EnterDataOutPhase(); +} + +//--------------------------------------------------------------------------- +// +// Enable or Disable the interface +// +// Command: 0e 00 00 00 00 XX (XX = 80 or 00) +// Function: Enable (80) / disable (00) Ethernet interface +// Type: No data transferred +// Notes: After issuing an Enable, the initiator should avoid sending +// any subsequent commands to the device for approximately 0.5 +// seconds +// +//--------------------------------------------------------------------------- +void SCSIDaynaPort::EnableInterface() const +{ + if (GetController()->GetCmdByte(5) & 0x80) { + if (const string error = tap.IpLink(true); !error.empty()) { + LogWarn("Unable to enable the DaynaPort Interface: " + error); + + throw scsi_exception(sense_key::aborted_command); + } + + tap.Flush(); + + LogInfo("The DaynaPort interface has been ENABLED"); + } + else { + if (const string error = tap.IpLink(false); !error.empty()) { + LogWarn("Unable to disable the DaynaPort Interface: " + error); + + throw scsi_exception(sense_key::aborted_command); + } + + LogInfo("The DaynaPort interface has been DISABLED"); + } + + EnterStatusPhase(); +} + +vector SCSIDaynaPort::GetStatistics() const +{ + vector statistics = PrimaryDevice::GetStatistics(); + + PbStatistics s; + s.set_id(GetId()); + s.set_unit(GetLun()); + + s.set_category(PbStatisticsCategory::CATEGORY_INFO); + + s.set_key(BYTE_READ_COUNT); + s.set_value(byte_read_count); + statistics.push_back(s); + + s.set_key(BYTE_WRITE_COUNT); + s.set_value(byte_write_count); + statistics.push_back(s); + + return statistics; +} diff --git a/cpp/devices/daynaport.h b/cpp/devices/daynaport.h new file mode 100644 index 00000000..990fc3c2 --- /dev/null +++ b/cpp/devices/daynaport.h @@ -0,0 +1,128 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2020 akuker +// Copyright (C) 2014-2020 GIMONS +// Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +// Copyright (C) 2023 Uwe Seimet +// +// This design is derived from the SLINKCMD.TXT file, as well as David Kuder's +// Tiny SCSI Emulator +// - SLINKCMD: http://www.bitsavers.org/pdf/apple/scsi/dayna/daynaPORT/SLINKCMD.TXT +// - Tiny SCSI : https://hackaday.io/project/18974-tiny-scsi-emulator +// +// Special thanks to @PotatoFi for loaning me his Farallon EtherMac for +// this development. (Farallon's EtherMac is a re-branded DaynaPort +// SCSI/Link-T). +// +// This does NOT include the file system functionality that is present +// in the Sharp X68000 host bridge. +// +// Note: This requires a DaynaPort SCSI Link driver. +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "base/primary_device.h" +#include "ctapdriver.h" +#ifndef __NetBSD__ +#include +#endif +#include +#include +#include +#include + +//=========================================================================== +// +// DaynaPort SCSI Link +// +//=========================================================================== +class SCSIDaynaPort : public PrimaryDevice +{ + uint64_t byte_read_count = 0; + uint64_t byte_write_count = 0; + + inline static const string BYTE_READ_COUNT = "byte_read_count"; + inline static const string BYTE_WRITE_COUNT = "byte_write_count"; + +public: + + explicit SCSIDaynaPort(int); + ~SCSIDaynaPort() override = default; + + bool Init(const param_map&) override; + void CleanUp() override; + + param_map GetDefaultParams() const override { return tap.GetDefaultParams(); } + + // Commands + vector InquiryInternal() const override; + int Read(cdb_t, vector&, uint64_t); + bool Write(cdb_t, span); + + int RetrieveStats(cdb_t, vector&) const; + + void TestUnitReady() override; + void Read6(); + void Write6() const; + void RetrieveStatistics() const; + void SetInterfaceMode() const; + void SetMcastAddr() const; + void EnableInterface() const; + + vector GetStatistics() const override; + + static const int CMD_SCSILINK_STATS = 0x09; + static const int CMD_SCSILINK_ENABLE = 0x0E; + static const int CMD_SCSILINK_SET = 0x0C; + static const int CMD_SCSILINK_SETMAC = 0x40; + static const int CMD_SCSILINK_SETMODE = 0x80; + + // When we're reading the Linux tap device, most of the messages will not be for us, so we + // need to filter through those. However, we don't want to keep re-reading the packets + // indefinitely. So, we'll pick a large-ish number that will cause the emulated DaynaPort + // to respond with "no data" after MAX_READ_RETRIES tries. + static const int MAX_READ_RETRIES = 50; + + // The READ response has a header which consists of: + // 2 bytes - payload size + // 4 bytes - status flags + static const uint32_t DAYNAPORT_READ_HEADER_SZ = 2 + 4; + +private: + + enum class read_data_flags_t : uint32_t { + e_no_more_data = 0x00000000, + e_more_data_available = 0x00000001, + e_dropped_packets = 0xFFFFFFFF, + }; + + using scsi_resp_read_t = struct __attribute__((packed)) { + uint32_t length; + read_data_flags_t flags; + byte pad; + array data; // Frame length + 4 byte CRC + }; + + using scsi_resp_link_stats_t = struct __attribute__((packed)) { + array mac_address; + uint32_t frame_alignment_errors; + uint32_t crc_errors; + uint32_t frames_lost; + }; + + scsi_resp_link_stats_t m_scsi_link_stats = { + // TODO Remove this hard-coded MAC address, see https://github.com/PiSCSI/piscsi/issues/598 + .mac_address = { byte{0x00}, byte{0x80}, byte{0x19}, byte{0x10}, byte{0x98}, byte{0xe3} }, + .frame_alignment_errors = 0, + .crc_errors = 0, + .frames_lost = 0, + }; + + CTapDriver tap; + + bool tap_enabled = false; +}; diff --git a/cpp/devices/disk.cpp b/cpp/devices/disk.cpp new file mode 100644 index 00000000..578045e5 --- /dev/null +++ b/cpp/devices/disk.cpp @@ -0,0 +1,748 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +// Copyright (C) 2014-2020 GIMONS +// +// XM6i +// Copyright (C) 2010-2015 isaki@NetBSD.org +// Copyright (C) 2010 Y.Sugahara +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/shared_exceptions.h" +#include "base/scsi_command_util.h" +#include "disk.h" + +using namespace scsi_defs; +using namespace scsi_command_util; + +bool Disk::Init(const param_map& params) +{ + StorageDevice::Init(params); + + // TODO CD/DVD should not inherit write methods + // REZERO implementation is identical with Seek + AddCommand(scsi_command::cmd_rezero, [this] { Seek(); }); + AddCommand(scsi_command::cmd_format_unit, [this] { FormatUnit(); }); + // REASSIGN BLOCKS implementation is identical with Seek + AddCommand(scsi_command::cmd_reassign_blocks, [this] { Seek(); }); + AddCommand(scsi_command::cmd_read6, [this] { Read6(); }); + AddCommand(scsi_command::cmd_write6, [this] { Write6(); }); + AddCommand(scsi_command::cmd_seek6, [this] { Seek6(); }); + AddCommand(scsi_command::cmd_start_stop, [this] { StartStopUnit(); }); + AddCommand(scsi_command::cmd_prevent_allow_medium_removal, [this]{ PreventAllowMediumRemoval(); }); + AddCommand(scsi_command::cmd_read_capacity10, [this] { ReadCapacity10(); }); + AddCommand(scsi_command::cmd_read10, [this] { Read10(); }); + AddCommand(scsi_command::cmd_write10, [this] { Write10(); }); + AddCommand(scsi_command::cmd_read_long10, [this] { ReadWriteLong10(); }); + AddCommand(scsi_command::cmd_write_long10, [this] { ReadWriteLong10(); }); + AddCommand(scsi_command::cmd_write_long16, [this] { ReadWriteLong16(); }); + AddCommand(scsi_command::cmd_seek10, [this] { Seek10(); }); + AddCommand(scsi_command::cmd_verify10, [this] { Verify10(); }); + AddCommand(scsi_command::cmd_synchronize_cache10, [this] { SynchronizeCache(); }); + AddCommand(scsi_command::cmd_synchronize_cache16, [this] { SynchronizeCache(); }); + AddCommand(scsi_command::cmd_read_defect_data10, [this] { ReadDefectData10(); }); + AddCommand(scsi_command::cmd_read16,[this] { Read16(); }); + AddCommand(scsi_command::cmd_write16, [this] { Write16(); }); + AddCommand(scsi_command::cmd_verify16, [this] { Verify16(); }); + AddCommand(scsi_command::cmd_read_capacity16_read_long16, [this] { ReadCapacity16_read_long16(); }); + + return true; +} + +void Disk::CleanUp() +{ + FlushCache(); + + StorageDevice::CleanUp(); +} + +void Disk::Dispatch(scsi_command cmd) +{ + // Media changes must be reported on the next access, i.e. not only for TEST UNIT READY + if (IsMediumChanged()) { + assert(IsRemovable()); + + SetMediumChanged(false); + + GetController()->Error(sense_key::unit_attention, asc::not_ready_to_ready_change); + } + else { + PrimaryDevice::Dispatch(cmd); + } +} + +void Disk::SetUpCache(off_t image_offset, bool raw) +{ + cache = make_unique(GetFilename(), size_shift_count, static_cast(GetBlockCount()), image_offset); + cache->SetRawMode(raw); +} + +void Disk::Resize_cache(const string& path, bool raw) +{ + cache.reset(new DiskCache(path, size_shift_count, static_cast(GetBlockCount()))); + cache->SetRawMode(raw); +} + +void Disk::FlushCache() +{ + if (cache != nullptr && IsReady()) { + cache->Save(); + } +} + +void Disk::FormatUnit() +{ + CheckReady(); + + // FMTDATA=1 is not supported (but OK if there is no DEFECT LIST) + if ((GetController()->GetCmdByte(1) & 0x10) != 0 && GetController()->GetCmdByte(4) != 0) { + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } + + EnterStatusPhase(); +} + +void Disk::Read(access_mode mode) +{ + const auto& [valid, start, blocks] = CheckAndGetStartAndCount(mode); + if (valid) { + GetController()->SetBlocks(blocks); + + const int length = Read(GetController()->GetBuffer(), start); + LogTrace("Length is " + to_string(length)); + GetController()->SetLength(length); + + // Set next block + GetController()->SetNext(start + 1); + + EnterDataInPhase(); + } + else { + EnterStatusPhase(); + } +} + +void Disk::ReadWriteLong10() const +{ + ValidateBlockAddress(RW10); + + // Transfer lengths other than 0 are not supported, which is compliant with the SCSI standard + if (GetInt16(GetController()->GetCmd(), 7) != 0) { + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } + + EnterStatusPhase(); +} + +void Disk::ReadWriteLong16() const +{ + ValidateBlockAddress(RW16); + + // Transfer lengths other than 0 are not supported, which is compliant with the SCSI standard + if (GetInt16(GetController()->GetCmd(), 12) != 0) { + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } + + EnterStatusPhase(); +} + +void Disk::Write(access_mode mode) const +{ + if (IsProtected()) { + throw scsi_exception(sense_key::data_protect, asc::write_protected); + } + + const auto& [valid, start, blocks] = CheckAndGetStartAndCount(mode); + if (valid) { + GetController()->SetBlocks(blocks); + GetController()->SetLength(GetSectorSizeInBytes()); + + // Set next block + GetController()->SetNext(start + 1); + + EnterDataOutPhase(); + } + else { + EnterStatusPhase(); + } +} + +void Disk::Verify(access_mode mode) +{ + const auto& [valid, start, blocks] = CheckAndGetStartAndCount(mode); + if (valid) { + // if BytChk=0 + if ((GetController()->GetCmdByte(1) & 0x02) == 0) { + Seek(); + return; + } + + // Test reading + GetController()->SetBlocks(blocks); + GetController()->SetLength(Read(GetController()->GetBuffer(), start)); + + // Set next block + GetController()->SetNext(start + 1); + + EnterDataOutPhase(); + } + else { + EnterStatusPhase(); + } +} + +void Disk::StartStopUnit() +{ + const bool start = GetController()->GetCmdByte(4) & 0x01; + const bool load = GetController()->GetCmdByte(4) & 0x02; + + if (load) { + LogTrace(start ? "Loading medium" : "Ejecting medium"); + } + else { + LogTrace(start ? "Starting unit" : "Stopping unit"); + + SetStopped(!start); + } + + if (!start) { + // Look at the eject bit and eject if necessary + if (load) { + if (IsLocked()) { + // Cannot be ejected because it is locked + throw scsi_exception(sense_key::illegal_request, asc::load_or_eject_failed); + } + + // Eject + if (!Eject(false)) { + throw scsi_exception(sense_key::illegal_request, asc::load_or_eject_failed); + } + } + else { + FlushCache(); + } + } + + EnterStatusPhase(); +} + +void Disk::PreventAllowMediumRemoval() +{ + CheckReady(); + + const bool lock = GetController()->GetCmdByte(4) & 0x01; + + LogTrace(lock ? "Locking medium" : "Unlocking medium"); + + SetLocked(lock); + + EnterStatusPhase(); +} + +void Disk::SynchronizeCache() +{ + FlushCache(); + + EnterStatusPhase(); +} + +void Disk::ReadDefectData10() const +{ + const size_t allocation_length = min(static_cast(GetInt16(GetController()->GetCmd(), 7)), + static_cast(4)); + + // The defect list is empty + fill_n(GetController()->GetBuffer().begin(), allocation_length, 0); + GetController()->SetLength(static_cast(allocation_length)); + + EnterDataInPhase(); +} + +bool Disk::Eject(bool force) +{ + const bool status = PrimaryDevice::Eject(force); + if (status) { + FlushCache(); + cache.reset(); + + // The image file for this drive is not in use anymore + UnreserveFile(); + + sector_read_count = 0; + sector_write_count = 0; + } + + return status; +} + +int Disk::ModeSense6(cdb_t cdb, vector& buf) const +{ + // Get length, clear buffer + const auto length = static_cast(min(buf.size(), static_cast(cdb[4]))); + fill_n(buf.begin(), length, 0); + + // DEVICE SPECIFIC PARAMETER + if (IsProtected()) { + buf[2] = 0x80; + } + + // Basic information + int size = 4; + + // Add block descriptor if DBD is 0 + if ((cdb[1] & 0x08) == 0) { + // Mode parameter header, block descriptor length + buf[3] = 0x08; + + // Only if ready + if (IsReady()) { + // Short LBA mode parameter block descriptor (number of blocks and block length) + SetInt32(buf, 4, static_cast(GetBlockCount())); + SetInt32(buf, 8, GetSectorSizeInBytes()); + } + + size = 12; + } + + size = AddModePages(cdb, buf, size, length, 255); + + buf[0] = (uint8_t)size; + + return size; +} + +int Disk::ModeSense10(cdb_t cdb, vector& buf) const +{ + // Get length, clear buffer + const auto length = static_cast(min(buf.size(), static_cast(GetInt16(cdb, 7)))); + fill_n(buf.begin(), length, 0); + + // DEVICE SPECIFIC PARAMETER + if (IsProtected()) { + buf[3] = 0x80; + } + + // Basic Information + int size = 8; + + // Add block descriptor if DBD is 0, only if ready + if ((cdb[1] & 0x08) == 0 && IsReady()) { + uint64_t disk_blocks = GetBlockCount(); + uint32_t disk_size = GetSectorSizeInBytes(); + + // Check LLBAA for short or long block descriptor + if ((cdb[1] & 0x10) == 0 || disk_blocks <= 0xFFFFFFFF) { + // Mode parameter header, block descriptor length + buf[7] = 0x08; + + // Short LBA mode parameter block descriptor (number of blocks and block length) + SetInt32(buf, 8, static_cast(disk_blocks)); + SetInt32(buf, 12, disk_size); + + size = 16; + } + else { + // Mode parameter header, LONGLBA + buf[4] = 0x01; + + // Mode parameter header, block descriptor length + buf[7] = 0x10; + + // Long LBA mode parameter block descriptor (number of blocks and block length) + SetInt64(buf, 8, disk_blocks); + SetInt32(buf, 20, disk_size); + + size = 24; + } + } + + size = AddModePages(cdb, buf, size, length, 65535); + + SetInt16(buf, 0, size); + + return size; +} + +void Disk::SetUpModePages(map>& pages, int page, bool changeable) const +{ + // Page code 1 (read-write error recovery) + if (page == 0x01 || page == 0x3f) { + AddErrorPage(pages, changeable); + } + + // Page code 3 (format device) + if (page == 0x03 || page == 0x3f) { + AddFormatPage(pages, changeable); + } + + // Page code 4 (rigid drive page) + if (page == 0x04 || page == 0x3f) { + AddDrivePage(pages, changeable); + } + + // Page code 8 (caching) + if (page == 0x08 || page == 0x3f) { + AddCachePage(pages, changeable); + } + + // Page (vendor special) + AddVendorPage(pages, page, changeable); +} + +void Disk::AddErrorPage(map>& pages, bool) const +{ + // Retry count is 0, limit time uses internal default value + vector buf(12); + + // TB, PER, DTE (required for OpenVMS/VAX compatibility, see issue #1117) + buf[2] = (byte)0x26; + + pages[1] = buf; +} + +void Disk::AddFormatPage(map>& pages, bool changeable) const +{ + vector buf(24); + + // No changeable area + if (changeable) { + pages[3] = buf; + + return; + } + + if (IsReady()) { + // Set the number of tracks in one zone to 8 + buf[0x03] = (byte)0x08; + + // Set sector/track to 25 + SetInt16(buf, 0x0a, 25); + + // Set the number of bytes in the physical sector + SetInt16(buf, 0x0c, 1 << size_shift_count); + + // Interleave 1 + SetInt16(buf, 0x0e, 1); + + // Track skew factor 11 + SetInt16(buf, 0x10, 11); + + // Cylinder skew factor 20 + SetInt16(buf, 0x12, 20); + } + + buf[20] = IsRemovable() ? (byte)0x20 : (byte)0x00; + + // Hard-sectored + buf[20] |= (byte)0x40; + + pages[3] = buf; +} + +void Disk::AddDrivePage(map>& pages, bool changeable) const +{ + vector buf(24); + + // No changeable area + if (changeable) { + pages[4] = buf; + + return; + } + + if (IsReady()) { + // Set the number of cylinders (total number of blocks + // divided by 25 sectors/track and 8 heads) + uint64_t cylinders = GetBlockCount(); + cylinders >>= 3; + cylinders /= 25; + SetInt32(buf, 0x01, static_cast(cylinders)); + + // Fix the head at 8 + buf[0x05] = (byte)0x8; + + // Medium rotation rate 7200 + SetInt16(buf, 0x14, 7200); + } + + pages[4] = buf; +} + +void Disk::AddCachePage(map>& pages, bool changeable) const +{ + vector buf(12); + + // No changeable area + if (changeable) { + pages[8] = buf; + + return; + } + + // Only read cache is valid + + // Disable pre-fetch transfer length + SetInt16(buf, 0x04, -1); + + // Maximum pre-fetch + SetInt16(buf, 0x08, -1); + + // Maximum pre-fetch ceiling + SetInt16(buf, 0x0a, -1); + + pages[8] = buf; +} + +int Disk::Read(span buf, uint64_t block) +{ + assert(block < GetBlockCount()); + + CheckReady(); + + if (!cache->ReadSector(buf, static_cast(block))) { + throw scsi_exception(sense_key::medium_error, asc::read_fault); + } + + ++sector_read_count; + + return GetSectorSizeInBytes(); +} + +void Disk::Write(span buf, uint64_t block) +{ + assert(block < GetBlockCount()); + + CheckReady(); + + if (!cache->WriteSector(buf, static_cast(block))) { + throw scsi_exception(sense_key::medium_error, asc::write_fault); + } + + ++sector_write_count; +} + +void Disk::Seek() +{ + CheckReady(); + + EnterStatusPhase(); +} + +void Disk::Seek6() +{ + const auto& [valid, start, blocks] = CheckAndGetStartAndCount(SEEK6); + if (valid) { + CheckReady(); + } + + EnterStatusPhase(); +} + +void Disk::Seek10() +{ + const auto& [valid, start, blocks] = CheckAndGetStartAndCount(SEEK10); + if (valid) { + CheckReady(); + } + + EnterStatusPhase(); +} + +void Disk::ReadCapacity10() +{ + CheckReady(); + + if (GetBlockCount() == 0) { + throw scsi_exception(sense_key::illegal_request, asc::medium_not_present); + } + + vector& buf = GetController()->GetBuffer(); + + // Create end of logical block address (blocks-1) + uint64_t capacity = GetBlockCount() - 1; + + // If the capacity exceeds 32 bit, -1 must be returned and the client has to use READ CAPACITY(16) + if (capacity > 4294967295) { + capacity = -1; + } + SetInt32(buf, 0, static_cast(capacity)); + + // Create block length (1 << size) + SetInt32(buf, 4, 1 << size_shift_count); + + GetController()->SetLength(8); + + EnterDataInPhase(); +} + +void Disk::ReadCapacity16() +{ + CheckReady(); + + if (GetBlockCount() == 0) { + throw scsi_exception(sense_key::illegal_request, asc::medium_not_present); + } + + vector& buf = GetController()->GetBuffer(); + + // Create end of logical block address (blocks-1) + SetInt64(buf, 0, GetBlockCount() - 1); + + // Create block length (1 << size) + SetInt32(buf, 8, 1 << size_shift_count); + + buf[12] = 0; + + // Logical blocks per physical block: not reported (1 or more) + buf[13] = 0; + + GetController()->SetLength(14); + + EnterDataInPhase(); +} + +void Disk::ReadCapacity16_read_long16() +{ + // The service action determines the actual command + switch (GetController()->GetCmdByte(1) & 0x1f) { + case 0x10: + ReadCapacity16(); + break; + + case 0x11: + ReadWriteLong16(); + break; + + default: + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + break; + } +} + +void Disk::ValidateBlockAddress(access_mode mode) const +{ + const uint64_t block = mode == RW16 ? GetInt64(GetController()->GetCmd(), 2) : GetInt32(GetController()->GetCmd(), 2); + + if (block > GetBlockCount()) { + LogTrace("Capacity of " + to_string(GetBlockCount()) + " block(s) exceeded: Trying to access block " + + to_string(block)); + throw scsi_exception(sense_key::illegal_request, asc::lba_out_of_range); + } +} + +tuple Disk::CheckAndGetStartAndCount(access_mode mode) const +{ + uint64_t start; + uint32_t count; + + if (mode == RW6 || mode == SEEK6) { + start = GetInt24(GetController()->GetCmd(), 1); + + count = GetController()->GetCmdByte(4); + if (!count) { + count= 0x100; + } + } + else { + start = mode == RW16 ? GetInt64(GetController()->GetCmd(), 2) : GetInt32(GetController()->GetCmd(), 2); + + if (mode == RW16) { + count = GetInt32(GetController()->GetCmd(), 10); + } + else if (mode != SEEK10) { + count = GetInt16(GetController()->GetCmd(), 7); + } + else { + count = 0; + } + } + + LogTrace(fmt::format("READ/WRITE/VERIFY/SEEK, start sector: ${0:x}, sector count: {1}", start, count)); + + // Check capacity + if (uint64_t capacity = GetBlockCount(); !capacity || start > capacity || start + count > capacity) { + LogTrace("Capacity of " + to_string(capacity) + " sector(s) exceeded: Trying to access sector " + + to_string(start) + ", sector count " + to_string(count)); + throw scsi_exception(sense_key::illegal_request, asc::lba_out_of_range); + } + + // Do not process 0 blocks + if (!count && (mode != SEEK6 && mode != SEEK10)) { + return tuple(false, start, count); + } + + return tuple(true, start, count); +} + +uint32_t Disk::CalculateShiftCount(uint32_t size_in_bytes) +{ + const auto& it = shift_counts.find(size_in_bytes); + return it != shift_counts.end() ? it->second : 0; +} + +uint32_t Disk::GetSectorSizeInBytes() const +{ + return size_shift_count ? 1 << size_shift_count : 0; +} + +void Disk::SetSectorSizeInBytes(uint32_t size_in_bytes) +{ + if (!GetSupportedSectorSizes().contains(size_in_bytes)) { + throw io_exception("Invalid sector size of " + to_string(size_in_bytes) + " byte(s)"); + } + + size_shift_count = CalculateShiftCount(size_in_bytes); + assert(size_shift_count); +} + +uint32_t Disk::GetConfiguredSectorSize() const +{ + return configured_sector_size; +} + +bool Disk::SetConfiguredSectorSize(uint32_t configured_size) +{ + if (!supported_sector_sizes.contains(configured_size)) { + return false; + } + + configured_sector_size = configured_size; + + return true; +} + +vector Disk::GetStatistics() const +{ + vector statistics = PrimaryDevice::GetStatistics(); + + // Enrich cache statistics with device information before adding them to device statistics + if (cache) { + for (auto& s : cache->GetStatistics(IsReadOnly())) { + s.set_id(GetId()); + s.set_unit(GetLun()); + statistics.push_back(s); + } + } + + PbStatistics s; + s.set_id(GetId()); + s.set_unit(GetLun()); + + s.set_category(PbStatisticsCategory::CATEGORY_INFO); + + s.set_key(SECTOR_READ_COUNT); + s.set_value(sector_read_count); + statistics.push_back(s); + + if (!IsReadOnly()) { + s.set_key(SECTOR_WRITE_COUNT); + s.set_value(sector_write_count); + statistics.push_back(s); + } + + return statistics; +} diff --git a/cpp/devices/disk.h b/cpp/devices/disk.h new file mode 100644 index 00000000..bf9f296c --- /dev/null +++ b/cpp/devices/disk.h @@ -0,0 +1,126 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +// Copyright (C) 2014-2020 GIMONS +// +// XMi: +// Copyright (C) 2010-2015 isaki@NetBSD.org +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "shared/s2p_util.h" +#include "disk_track.h" +#include "disk_cache.h" +#include "base/interfaces/scsi_block_commands.h" +#include "storage_device.h" +#include +#include +#include +#include +#include + +using namespace std; + +class Disk : public StorageDevice, private ScsiBlockCommands +{ + enum access_mode { RW6, RW10, RW16, SEEK6, SEEK10 }; + + unique_ptr cache; + + unordered_set supported_sector_sizes; + uint32_t configured_sector_size = 0; + + // Sector size shift count (9=512, 10=1024, 11=2048, 12=4096) + uint32_t size_shift_count = 0; + + uint64_t sector_read_count = 0; + uint64_t sector_write_count = 0; + + inline static const string SECTOR_READ_COUNT = "sector_read_count"; + inline static const string SECTOR_WRITE_COUNT = "sector_write_count"; + +public: + + Disk(PbDeviceType type, int lun, const unordered_set& s) + : StorageDevice(type, lun), supported_sector_sizes(s) {} + ~Disk() override = default; + + bool Init(const param_map&) override; + void CleanUp() override; + + void Dispatch(scsi_command) override; + + bool Eject(bool) override; + + virtual void Write(span, uint64_t); + + virtual int Read(span , uint64_t); + + uint32_t GetSectorSizeInBytes() const; + bool IsSectorSizeConfigurable() const { return supported_sector_sizes.size() > 1; } + const auto& GetSupportedSectorSizes() const { return supported_sector_sizes; } + bool SetConfiguredSectorSize(uint32_t); + void FlushCache() override; + + vector GetStatistics() const override; + +private: + + // Commands covered by the SCSI specifications (see https://www.t10.org/drafts.htm) + void StartStopUnit(); + void PreventAllowMediumRemoval(); + void SynchronizeCache(); + void ReadDefectData10() const; + virtual void Read6() { Read(RW6); } + void Read10() override { Read(RW10); } + void Read16() override { Read(RW16); } + virtual void Write6() { Write(RW6); } + void Write10() override { Write(RW10); } + void Write16() override { Write(RW16); } + void Verify10() { Verify(RW10); } + void Verify16() { Verify(RW16); } + void Seek(); + void Seek10(); + void ReadCapacity10() override; + void ReadCapacity16() override; + void FormatUnit() override; + void Seek6(); + void Read(access_mode); + void Write(access_mode) const; + void Verify(access_mode); + void ReadWriteLong10() const; + void ReadWriteLong16() const; + void ReadCapacity16_read_long16(); + + void ValidateBlockAddress(access_mode) const; + tuple CheckAndGetStartAndCount(access_mode) const; + + int ModeSense6(cdb_t, vector&) const override; + int ModeSense10(cdb_t, vector&) const override; + + static inline const unordered_map shift_counts = + { { 512, 9 }, { 1024, 10 }, { 2048, 11 }, { 4096, 12 } }; + +protected: + + void SetUpCache(off_t, bool = false); + void Resize_cache(const string&, bool); + + void SetUpModePages(map>&, int, bool) const override; + void AddErrorPage(map>&, bool) const; + virtual void AddFormatPage(map>&, bool) const; + virtual void AddDrivePage(map>&, bool) const; + void AddCachePage(map>&, bool) const; + + unordered_set GetSectorSizes() const; + void SetSectorSizeInBytes(uint32_t); + uint32_t GetSectorSizeShiftCount() const { return size_shift_count; } + void SetSectorSizeShiftCount(uint32_t count) { size_shift_count = count; } + uint32_t GetConfiguredSectorSize() const; + static uint32_t CalculateShiftCount(uint32_t); +}; diff --git a/cpp/devices/disk_cache.cpp b/cpp/devices/disk_cache.cpp new file mode 100644 index 00000000..ba9882f6 --- /dev/null +++ b/cpp/devices/disk_cache.cpp @@ -0,0 +1,220 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +// Copyright (C) 2014-2020 GIMONS +// +// XM6i +// Copyright (C) 2010-2015 isaki@NetBSD.org +// Copyright (C) 2010 Y.Sugahara +// +//--------------------------------------------------------------------------- + +#include "disk_track.h" +#include "disk_cache.h" +#include +#include +#include + +DiskCache::DiskCache(const string& path, int size, uint32_t blocks, off_t imgoff) + : sec_path(path), sec_size(size), sec_blocks(blocks), imgoffset(imgoff) +{ + assert(blocks > 0); + assert(imgoff >= 0); +} + +bool DiskCache::Save() +{ + // Save valid tracks + return ranges::none_of(cache.begin(), cache.end(), [this](const cache_t& c) + { return c.disktrk != nullptr && !c.disktrk->Save(sec_path, cache_miss_write_count); }); +} + +shared_ptr DiskCache::GetTrack(uint32_t block) +{ + // Update first + UpdateSerialNumber(); + + // Calculate track (fixed to 256 sectors/track) + int track = block >> 8; + + // Get track data + return Assign(track); +} + +bool DiskCache::ReadSector(span buf, uint32_t block) +{ + shared_ptr disktrk = GetTrack(block); + if (disktrk == nullptr) { + return false; + } + + // Read the track data to the cache + return disktrk->ReadSector(buf, block & 0xff); +} + +bool DiskCache::WriteSector(span buf, uint32_t block) +{ + shared_ptr disktrk = GetTrack(block); + if (disktrk == nullptr) { + return false; + } + + // Write the data to the cache + return disktrk->WriteSector(buf, block & 0xff); +} + +//--------------------------------------------------------------------------- +// +// Track Assignment +// +//--------------------------------------------------------------------------- +shared_ptr DiskCache::Assign(int track) +{ + assert(sec_size != 0); + assert(track >= 0); + + // First, check if it is already assigned + for (cache_t& c : cache) { + if (c.disktrk && c.disktrk->GetTrack() == track) { + // Track match + c.serial = serial; + return c.disktrk; + } + } + + // Next, check for empty + for (size_t i = 0; i < cache.size(); i++) { + if (cache[i].disktrk == nullptr) { + // Try loading + if (Load(static_cast(i), track, nullptr)) { + // Success loading + cache[i].serial = serial; + return cache[i].disktrk; + } + + // Load failed + return nullptr; + } + } + + // Finally, find the youngest serial number and delete it + + // Set index 0 as candidate c + uint32_t s = cache[0].serial; + size_t c = 0; + + // Compare candidate with serial and update to smaller one + for (size_t i = 0; i < cache.size(); i++) { + assert(cache[i].disktrk); + + // Compare and update the existing serial + if (cache[i].serial < s) { + s = cache[i].serial; + c = i; + } + } + + // Save this track + if (!cache[c].disktrk->Save(sec_path, cache_miss_write_count)) { + return nullptr; + } + + // Delete this track + shared_ptr disktrk = cache[c].disktrk; + cache[c].disktrk.reset(); + + if (Load(static_cast(c), track, disktrk)) { + // Successful loading + cache[c].serial = serial; + return cache[c].disktrk; + } + + // Load failed + return nullptr; +} + +//--------------------------------------------------------------------------- +// +// Load cache +// +//--------------------------------------------------------------------------- +bool DiskCache::Load(int index, int track, shared_ptr disktrk) +{ + assert(index >= 0 && index < static_cast(cache.size())); + assert(track >= 0); + assert(cache[index].disktrk == nullptr); + + // Get the number of sectors on this track + int sectors = sec_blocks - (track << 8); + assert(sectors > 0); + if (sectors > 0x100) { + sectors = 0x100; + } + + if (disktrk == nullptr) { + disktrk = make_shared(); + } + + disktrk->Init(track, sec_size, sectors, cd_raw, imgoffset); + + // Try loading + if (!disktrk->Load(sec_path, cache_miss_read_count)) { + ++read_error_count; + + return false; + } + + // Allocation successful, work set + cache[index].disktrk = disktrk; + + return true; +} + +void DiskCache::UpdateSerialNumber() +{ + // Update and do nothing except 0 + serial++; + if (serial != 0) { + return; + } + + // Clear serial of all caches + for (cache_t& c : cache) { + c.serial = 0; + } +} + +vector DiskCache::GetStatistics(bool is_read_only) const +{ + vector statistics; + + PbStatistics s; + + s.set_category(PbStatisticsCategory::CATEGORY_INFO); + + s.set_key(CACHE_MISS_READ_COUNT); + s.set_value(cache_miss_read_count); + statistics.push_back(s); + + if (!is_read_only) { + s.set_key(CACHE_MISS_WRITE_COUNT); + s.set_value(cache_miss_write_count); + statistics.push_back(s); + } + + s.set_category(PbStatisticsCategory::CATEGORY_ERROR); + + s.set_key(READ_ERROR_COUNT); + s.set_value(read_error_count); + statistics.push_back(s); + + if (!is_read_only) { + s.set_key(WRITE_ERROR_COUNT); + s.set_value(write_error_count); + statistics.push_back(s); + } + + return statistics; +} diff --git a/cpp/devices/disk_cache.h b/cpp/devices/disk_cache.h new file mode 100644 index 00000000..b782c067 --- /dev/null +++ b/cpp/devices/disk_cache.h @@ -0,0 +1,74 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +// Copyright (C) 2014-2020 GIMONS +// +// XM6i +// Copyright (C) 2010-2015 isaki@NetBSD.org +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "generated/command_interface.pb.h" +#include +#include +#include +#include + +using namespace std; +using namespace command_interface; + +class DiskCache +{ + // Number of tracks to cache + static const int CACHE_MAX = 16; + + uint64_t read_error_count = 0; + uint64_t write_error_count = 0; + uint64_t cache_miss_read_count = 0; + uint64_t cache_miss_write_count = 0; + + inline static const string READ_ERROR_COUNT = "read_error_count"; + inline static const string WRITE_ERROR_COUNT = "write_error_count"; + inline static const string CACHE_MISS_READ_COUNT = "cache_miss_read_count"; + inline static const string CACHE_MISS_WRITE_COUNT = "cache_miss_write_count"; + +public: + + using cache_t = struct { + shared_ptr disktrk; + uint32_t serial; + }; + + DiskCache(const string&, int, uint32_t, off_t = 0); + ~DiskCache() = default; + + void SetRawMode(bool b) { cd_raw = b; } + + bool Save(); + bool ReadSector(span, uint32_t); + bool WriteSector(span, uint32_t); + + vector GetStatistics(bool) const; + +private: + + shared_ptr Assign(int); + shared_ptr GetTrack(uint32_t); + bool Load(int index, int track, shared_ptr); + void UpdateSerialNumber(); + + // Internal datay + array cache = {}; // Cache management + uint32_t serial = 0; // Last serial number + string sec_path; // Path + int sec_size; // Sector Size (8=256, 9=512, 10=1024, 11=2048, 12=4096) + int sec_blocks; // Blocks per sector + bool cd_raw = false; // CD-ROM RAW mode + off_t imgoffset; // Offset to actual data +}; + diff --git a/cpp/devices/disk_track.cpp b/cpp/devices/disk_track.cpp new file mode 100644 index 00000000..6b87f8c9 --- /dev/null +++ b/cpp/devices/disk_track.cpp @@ -0,0 +1,279 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +// Copyright (C) 2014-2020 GIMONS +// +// XM6i +// Copyright (C) 2010-2015 isaki@NetBSD.org +// Copyright (C) 2010 Y.Sugahara +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "disk_track.h" +#include +#include +#include +#include + +DiskTrack::~DiskTrack() +{ + // Release memory, but do not save automatically + free(dt.buffer); +} + +void DiskTrack::Init(int track, int size, int sectors, bool raw, off_t imgoff) +{ + assert(track >= 0); + assert((sectors > 0) && (sectors <= 0x100)); + assert(imgoff >= 0); + + // Set Parameters + dt.track = track; + dt.size = size; + dt.sectors = sectors; + dt.raw = raw; + + // Not initialized (needs to be loaded) + dt.init = false; + + // Not Changed + dt.changed = false; + + // Offset to actual data + dt.imgoffset = imgoff; +} + +bool DiskTrack::Load(const string& path, uint64_t& cache_miss_read_count) +{ + // Not needed if already loaded + if (dt.init) { + assert(dt.buffer); + return true; + } + + ++cache_miss_read_count; + + // Calculate offset (previous tracks are considered to hold 256 sectors) + off_t offset = ((off_t)dt.track << 8); + if (dt.raw) { + assert(dt.size == 11); + offset *= 0x930; + offset += 0x10; + } else { + offset <<= dt.size; + } + + // Add offset to real image + offset += dt.imgoffset; + + // Calculate length (data size of this track) + const int length = dt.sectors << dt.size; + + // Allocate buffer memory + assert((dt.sectors > 0) && (dt.sectors <= 0x100)); + + if (dt.buffer == nullptr) { + if (posix_memalign((void **)&dt.buffer, 512, ((length + 511) / 512) * 512)) { + spdlog::warn("posix_memalign failed"); + } + dt.length = length; + } + + if (dt.buffer == nullptr) { + return false; + } + + // Reallocate if the buffer length is different + if (dt.length != static_cast(length)) { + free(dt.buffer); + if (posix_memalign((void **)&dt.buffer, 512, ((length + 511) / 512) * 512)) { + spdlog::warn("posix_memalign failed"); + } + dt.length = length; + } + + // Resize and clear changemap + dt.changemap.resize(dt.sectors); + fill(dt.changemap.begin(), dt.changemap.end(), false); //NOSONAR ranges::fill() cannot be applied to vector + + ifstream in(path, ios::binary); + if (in.fail()) { + return false; + } + + if (dt.raw) { + // Split Reading + for (int i = 0; i < dt.sectors; i++) { + in.seekg(offset); + if (in.fail()) { + return false; + } + + in.read((char *)&dt.buffer[i << dt.size], 1 << dt.size); + if (in.fail()) { + return false; + } + + // Next offset + offset += 0x930; + } + } else { + // Continuous reading + in.seekg(offset); + if (in.fail()) { + return false; + } + in.read((char *)dt.buffer, length); + if (in.fail()) { + return false; + } + } + + // Set a flag and end normally + dt.init = true; + dt.changed = false; + return true; +} + +bool DiskTrack::Save(const string& path, uint64_t& cache_miss_write_count) +{ + // Not needed if not initialized + if (!dt.init) { + return true; + } + + // Not needed unless changed + if (!dt.changed) { + return true; + } + + ++cache_miss_write_count; + + // Need to write + assert(dt.buffer); + assert((dt.sectors > 0) && (dt.sectors <= 0x100)); + + // Writing in RAW mode is not allowed + assert(!dt.raw); + + // Calculate offset (previous tracks are considered to hold 256 sectors) + off_t offset = ((off_t)dt.track << 8); + offset <<= dt.size; + + // Add offset to real image + offset += dt.imgoffset; + + // Calculate length per sector + const int length = 1 << dt.size; + + ofstream out(path, ios::in | ios::out | ios::binary); + if (out.fail()) { + return false; + } + + // Partial write loop + int total; + for (int i = 0; i < dt.sectors;) { + // If changed + if (dt.changemap[i]) { + // Initialize write size + total = 0; + + out.seekp(offset + ((off_t)i << dt.size)); + if (out.fail()) { + return false; + } + + // Consectutive sector length + int j; + for (j = i; j < dt.sectors; j++) { + // end when interrupted + if (!dt.changemap[j]) { + break; + } + + // Add one sector + total += length; + } + + out.write((const char *)&dt.buffer[i << dt.size], total); + if (out.fail()) { + return false; + } + + // To unmodified sector + i = j; + } else { + // Next Sector + i++; + } + } + + // Drop the change flag and exit + fill(dt.changemap.begin(), dt.changemap.end(), false); //NOSONAR ranges::fill() cannot be applied to vector + dt.changed = false; + + return true; +} + +bool DiskTrack::ReadSector(span buf, int sec) const +{ + assert(sec >= 0 && sec < 0x100); + + // Error if not initialized + if (!dt.init) { + return false; + } + + // // Error if the number of sectors exceeds the valid number + if (sec >= dt.sectors) { + return false; + } + + // Copy + assert(dt.buffer); + assert((dt.sectors > 0) && (dt.sectors <= 0x100)); + memcpy(buf.data(), &dt.buffer[(off_t)sec << dt.size], (off_t)1 << dt.size); + + // Success + return true; +} + +bool DiskTrack::WriteSector(span buf, int sec) +{ + assert((sec >= 0) && (sec < 0x100)); + assert(!dt.raw); + + // Error if not initialized + if (!dt.init) { + return false; + } + + // // Error if the number of sectors exceeds the valid number + if (sec >= dt.sectors) { + return false; + } + + // Calculate offset and length + const int offset = sec << dt.size; + const int length = 1 << dt.size; + + // Compare + assert(dt.buffer); + assert((dt.sectors > 0) && (dt.sectors <= 0x100)); + if (memcmp(buf.data(), &dt.buffer[offset], length) == 0) { + // Exit normally since it's attempting to write the same thing + return true; + } + + // Copy, change + memcpy(&dt.buffer[offset], buf.data(), length); + dt.changemap[sec] = true; + dt.changed = true; + + // Success + return true; +} diff --git a/cpp/devices/disk_track.h b/cpp/devices/disk_track.h new file mode 100644 index 00000000..f7c3af36 --- /dev/null +++ b/cpp/devices/disk_track.h @@ -0,0 +1,57 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +// Copyright (C) 2014-2020 GIMONS +// +// XM6i +// Copyright (C) 2010-2015 isaki@NetBSD.org +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include +#include +#include +#include + +using namespace std; + +class DiskTrack +{ + struct { + int track; // Track Number + int size; // Sector Size (8=256, 9=512, 10=1024, 11=2048, 12=4096) + int sectors; // Number of sectors(<0x100) + uint32_t length; // Data buffer length + uint8_t *buffer; // Data buffer + bool init; // Is it initilized? + bool changed; // Changed flag + vector changemap; // Changed map + bool raw; // RAW mode flag + off_t imgoffset; // Offset to actual data + } dt = { }; + +public: + + DiskTrack() = default; + ~DiskTrack(); + DiskTrack(DiskTrack&) = delete; + DiskTrack& operator=(const DiskTrack&) = delete; + +private: + + friend class DiskCache; + + void Init(int track, int size, int sectors, bool raw = false, off_t imgoff = 0); + bool Load(const string& path, uint64_t&); + bool Save(const string& path, uint64_t&); + + bool ReadSector(span, int) const; + bool WriteSector(span buf, int); + + int GetTrack() const { return dt.track; } +}; diff --git a/cpp/devices/host_services.cpp b/cpp/devices/host_services.cpp new file mode 100644 index 00000000..0e9c53b5 --- /dev/null +++ b/cpp/devices/host_services.cpp @@ -0,0 +1,348 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +// Host Services with support for realtime clock, shutdown and command execution +// +//--------------------------------------------------------------------------- + +// +// Features of the host services device: +// +// 1. Vendor-specific mode page 0x20 returns the current date and time, see mode_page_datetime +// +// 2. START/STOP UNIT shuts down s2p or shuts down/reboots the Raspberry Pi +// a) !start && !load (STOP): Shut down s2p +// b) !start && load (EJECT): Shut down the Raspberry Pi +// c) start && load (LOAD): Reboot the Raspberry Pi +// +// 3. Remote command execution via SCSI, using these vendor-specific SCSI commands: +// +// a) ExecuteOperation +// +// +============================================================================== +// | Bit| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | +// |Byte | | | | | | | | | +// |=====+======================================================================== +// | 0 | Operation code (c0h) | +// |-----+-----------------------------------------------------------------------| +// | 1 | Logical unit number | Reserved | TEXT | JSON | BIN | +// |-----+-----------------------------------------------------------------------| +// | 2 | Reserved | +// |-----+-----------------------------------------------------------------------| +// | 3 | Reserved | +// |-----+-----------------------------------------------------------------------| +// | 4 | Reserved | +// |-----+-----------------------------------------------------------------------| +// | 5 | Reserved | +// |-----+-----------------------------------------------------------------------| +// | 6 | Reserved | +// |-----+-----------------------------------------------------------------------| +// | 7 | (MSB) | +// |-----+--- Byte transfer length | +// | 8 | (LSB) | +// |-----+-----------------------------------------------------------------------| +// | 9 | Control | +// +============================================================================== +// +// b) ReceiveOperationResults +// +// +============================================================================== +// | Bit| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | +// |Byte | | | | | | | | | +// |=====+======================================================================== +// | 0 | Operation code (c1h) | +// |-----+-----------------------------------------------------------------------| +// | 1 | Logical unit number | Reserved | TEXT | JSON | BIN | +// |-----+-----------------------------------------------------------------------| +// | 2 | Reserved | +// |-----+-----------------------------------------------------------------------| +// | 3 | Reserved | +// |-----+-----------------------------------------------------------------------| +// | 4 | Reserved | +// |-----+-----------------------------------------------------------------------| +// | 5 | Reserved | +// |-----+-----------------------------------------------------------------------| +// | 6 | Reserved | +// |-----+-----------------------------------------------------------------------| +// | 7 | (MSB) | +// |-----+--- Byte transfer length | +// | 8 | (LSB) | +// |-----+-----------------------------------------------------------------------| +// | 9 | Control | +// +============================================================================== +// +// The remote interface commands that can be executed are defined in the command_interface.proto file. +// The BIN, JSON and TEXT flags control the input and output format of the protobuf data. +// Exactly one of them must be set. Input and output format do not have to be identical. +// ReceiveOperationResults returns the result of the last operation executed. +// + +#include "shared/shared_exceptions.h" +#include "shared/protobuf_util.h" +#include "controllers/scsi_controller.h" +#include "base/scsi_command_util.h" +#include "host_services.h" +#include "generated/command_interface.pb.h" +#include +#include +#include +#include + +using namespace std::chrono; +using namespace google::protobuf; +using namespace google::protobuf::util; +using namespace command_interface; +using namespace scsi_defs; +using namespace scsi_command_util; +using namespace protobuf_util; + +bool HostServices::Init(const param_map& params) +{ + ModePageDevice::Init(params); + + AddCommand(scsi_command::cmd_test_unit_ready, [this] { TestUnitReady(); }); + AddCommand(scsi_command::cmd_start_stop, [this] { StartStopUnit(); }); + AddCommand(scsi_command::cmd_execute_operation, [this] { ExecuteOperation(); }); + AddCommand(scsi_command::cmd_receive_operation_results, [this] { ReceiveOperationResults(); }); + + SetReady(true); + + return true; +} + +void HostServices::TestUnitReady() +{ + // Always successful + EnterStatusPhase(); +} + +vector HostServices::InquiryInternal() const +{ + return HandleInquiry(device_type::processor, scsi_level::spc_3, false); +} + +void HostServices::StartStopUnit() const +{ + const bool start = GetController()->GetCmdByte(4) & 0x01; + const bool load = GetController()->GetCmdByte(4) & 0x02; + + if (!start) { + if (load) { + GetController()->ScheduleShutdown(AbstractController::shutdown_mode::STOP_PI); + } + else { + GetController()->ScheduleShutdown(AbstractController::shutdown_mode::STOP_S2P); + } + } + else if (load) { + GetController()->ScheduleShutdown(AbstractController::shutdown_mode::RESTART_PI); + } + else { + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } + + EnterStatusPhase(); +} + +void HostServices::ExecuteOperation() +{ + execution_results.erase(GetController()->GetInitiatorId()); + + input_format = ConvertFormat(); + + const auto length = static_cast(GetInt16(GetController()->GetCmd(), 7)); + if (!length) { + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } + + GetController()->SetLength(static_cast(length)); + GetController()->SetByteTransfer(true); + + EnterDataOutPhase(); +} + +void HostServices::ReceiveOperationResults() +{ + const protobuf_format output_format = ConvertFormat(); + + const auto& it = execution_results.find(GetController()->GetInitiatorId()); + if (it == execution_results.end()) { + throw scsi_exception(sense_key::aborted_command); + } + const string& execution_result = it->second; + + string data; + switch (output_format) { + case protobuf_format::binary: + data = execution_result; + break; + + case protobuf_format::json: { + PbResult result; + result.ParseFromArray(execution_result.data(), static_cast(execution_result.size())); + MessageToJsonString(result, &data).ok(); + break; + } + + case protobuf_format::text: { + PbResult result; + result.ParseFromArray(execution_result.data(), static_cast(execution_result.size())); + TextFormat::PrintToString(result, &data); + break; + } + + default: + assert(false); + break; + } + + execution_results.erase(GetController()->GetInitiatorId()); + + const auto allocation_length = static_cast(GetInt16(GetController()->GetCmd(), 7)); + const auto length = static_cast(min(allocation_length, data.size())); + if (!length) { + EnterStatusPhase(); + } + else { + GetController()->CopyToBuffer(data.data(), length); + + EnterDataInPhase(); + } +} + +int HostServices::ModeSense6(cdb_t cdb, vector& buf) const +{ + // Block descriptors cannot be returned + if (!(cdb[1] & 0x08)) { + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } + + const auto length = static_cast(min(buf.size(), static_cast(cdb[4]))); + fill_n(buf.begin(), length, 0); + + // 4 bytes basic information + const int size = AddModePages(cdb, buf, 4, length, 255); + + buf[0] = (uint8_t)size; + + return size; +} + +int HostServices::ModeSense10(cdb_t cdb, vector& buf) const +{ + // Block descriptors cannot be returned + if (!(cdb[1] & 0x08)) { + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } + + const auto length = static_cast(min(buf.size(), static_cast(GetInt16(cdb, 7)))); + fill_n(buf.begin(), length, 0); + + // 8 bytes basic information + const int size = AddModePages(cdb, buf, 8, length, 65535); + + SetInt16(buf, 0, size); + + return size; +} + +void HostServices::SetUpModePages(map>& pages, int page, bool changeable) const +{ + if (page == 0x20 || page == 0x3f) { + AddRealtimeClockPage(pages, changeable); + } +} + +void HostServices::AddRealtimeClockPage(map>& pages, bool changeable) const +{ + pages[32] = vector(10); + + if (!changeable) { + const auto now = system_clock::now(); + const time_t t = system_clock::to_time_t(now); + tm localtime; + localtime_r(&t, &localtime); + + mode_page_datetime datetime; + datetime.major_version = 0x01; + datetime.minor_version = 0x00; + datetime.year = (uint8_t)localtime.tm_year; + datetime.month = (uint8_t)localtime.tm_mon; + datetime.day = (uint8_t)localtime.tm_mday; + datetime.hour = (uint8_t)localtime.tm_hour; + datetime.minute = (uint8_t)localtime.tm_min; + // Ignore leap second for simplicity + datetime.second = (uint8_t)(localtime.tm_sec < 60 ? localtime.tm_sec : 59); + + memcpy(&pages[32][2], &datetime, sizeof(datetime)); + } +} + +bool HostServices::WriteByteSequence(span buf) +{ + const auto length = GetInt16(GetController()->GetCmd(), 7); + + PbCommand command; + switch (input_format) { + case protobuf_format::binary: + if (!command.ParseFromArray(buf.data(), length)) { + LogTrace("Failed to deserialize protobuf binary data"); + return false; + } + break; + + case protobuf_format::json: { + if (string cmd((const char*) buf.data(), length); !JsonStringToMessage(cmd, &command).ok()) { + LogTrace("Failed to deserialize protobuf JSON data"); + return false; + } + break; + } + + case protobuf_format::text: { + if (string cmd((const char*) buf.data(), length); !TextFormat::ParseFromString(cmd, &command)) { + LogTrace("Failed to deserialize protobuf text format data"); + return false; + } + break; + } + + default: + assert(false); + break; + } + + PbResult result; + if (CommandContext context(command, s2p_image.GetDefaultFolder(), protobuf_util::GetParam(command, "locale")); + !dispatcher->DispatchCommand(context, result, fmt::format("(ID:LUN {0}:{1}) - ", GetId(), GetLun()))) { + LogTrace("Failed to execute " + PbOperation_Name(command.operation()) + " operation"); + return false; + } + + execution_results[GetController()->GetInitiatorId()] = result.SerializeAsString(); + + return true; +} + +HostServices::protobuf_format HostServices::ConvertFormat() const +{ + switch (GetController()->GetCmdByte(1) & 0b00000111) { + case 0x001: + return protobuf_format::binary; + break; + + case 0b010: + return protobuf_format::json; + break; + + case 0b100: + return protobuf_format::text; + break; + + default: + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } +} diff --git a/cpp/devices/host_services.h b/cpp/devices/host_services.h new file mode 100644 index 00000000..77f205f8 --- /dev/null +++ b/cpp/devices/host_services.h @@ -0,0 +1,87 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +// Host Services with support for realtime clock, shutdown and command execution +// +//--------------------------------------------------------------------------- + +#pragma once + +// TODO Move the shared code to a new library +#include "s2p/command_context.h" +#include "s2p/command_dispatcher.h" +#include "s2p/s2p_image.h" +#include "mode_page_device.h" +#include +#include +#include + +using namespace std; +using namespace command_interface; + +class HostServices: public ModePageDevice +{ + inline static const int EXECUTE_BUFFER_SIZE = 65535; + +public: + + explicit HostServices(int lun) : ModePageDevice(SCHS, lun) {} + ~HostServices() override = default; + + bool Init(const param_map&) override; + + vector InquiryInternal() const override; + void TestUnitReady() override; + + void SetDispatcher(shared_ptr d) { dispatcher = d; } + +protected: + + void SetUpModePages(map>&, int, bool) const override; + +private: + + enum class protobuf_format { + binary = 0b001, + json = 0b010, + text = 0b100 + }; + + using mode_page_datetime = struct __attribute__((packed)) { + // Major and minor version of this data structure (e.g. 1.0) + uint8_t major_version; + uint8_t minor_version; + // Current date and time, with daylight savings time adjustment applied + uint8_t year; // year - 1900 + uint8_t month; // 0-11 + uint8_t day; // 1-31 + uint8_t hour; // 0-23 + uint8_t minute; // 0-59 + uint8_t second; // 0-59 + }; + + void StartStopUnit() const; + void ExecuteOperation(); + void ReceiveOperationResults(); + + int ModeSense6(cdb_t, vector&) const override; + int ModeSense10(cdb_t, vector&) const override; + + void AddRealtimeClockPage(map>&, bool) const; + + bool WriteByteSequence(span) override; + + protobuf_format ConvertFormat() const; + + // Operation results per initiator + unordered_map execution_results; + + shared_ptr dispatcher; + + S2pImage s2p_image; + + protobuf_format input_format = protobuf_format::binary; +}; diff --git a/cpp/devices/mode_page_device.cpp b/cpp/devices/mode_page_device.cpp new file mode 100644 index 00000000..24300489 --- /dev/null +++ b/cpp/devices/mode_page_device.cpp @@ -0,0 +1,139 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +// A basic device with mode page support, to be used for subclassing +// +//--------------------------------------------------------------------------- + +#include "shared/shared_exceptions.h" +#include "base/scsi_command_util.h" +#include "mode_page_device.h" +#include +#include + +using namespace std; +using namespace scsi_defs; +using namespace scsi_command_util; + +bool ModePageDevice::Init(const param_map& params) +{ + PrimaryDevice::Init(params); + + AddCommand(scsi_command::cmd_mode_sense6, [this] { ModeSense6(); }); + AddCommand(scsi_command::cmd_mode_sense10, [this] { ModeSense10(); }); + AddCommand(scsi_command::cmd_mode_select6, [this] { ModeSelect6(); }); + AddCommand(scsi_command::cmd_mode_select10, [this] { ModeSelect10(); }); + + return true; +} + +int ModePageDevice::AddModePages(cdb_t cdb, vector& buf, int offset, int length, int max_size) const +{ + const int max_length = length - offset; + if (max_length < 0) { + return length; + } + + const bool changeable = (cdb[2] & 0xc0) == 0x40; + + // Get page code (0x3f means all pages) + const int page = cdb[2] & 0x3f; + + LogTrace(fmt::format("Requesting mode page ${:02x}", page)); + + // Mode page data mapped to the respective page numbers, C++ maps are ordered by key + map> pages; + SetUpModePages(pages, page, changeable); + + if (pages.empty()) { + LogTrace(fmt::format("Unsupported mode page ${:02x}", page)); + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } + + // Holds all mode page data + vector result; + + vector page0; + for (const auto& [index, data] : pages) { + // The specification mandates that page 0 must be returned after all others + if (index) { + const size_t off = result.size(); + + // Page data + result.insert(result.end(), data.begin(), data.end()); + // Page code, PS bit may already have been set + result[off] |= (byte)index; + // Page payload size + result[off + 1] = (byte)(data.size() - 2); + } + else { + page0 = data; + } + } + + // Page 0 must be last + if (!page0.empty()) { + const size_t off = result.size(); + + // Page data + result.insert(result.end(), page0.begin(), page0.end()); + // Page payload size + result[off + 1] = (byte)(page0.size() - 2); + } + + if (static_cast(result.size()) > max_size) { + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } + + const auto size = static_cast(min(static_cast(max_length), result.size())); + memcpy(&buf.data()[offset], result.data(), size); + + // Do not return more than the requested number of bytes + return size + offset < length ? size + offset : length; +} + +void ModePageDevice::ModeSense6() const +{ + GetController()->SetLength(ModeSense6(GetController()->GetCmd(), GetController()->GetBuffer())); + + EnterDataInPhase(); +} + +void ModePageDevice::ModeSense10() const +{ + GetController()->SetLength(ModeSense10(GetController()->GetCmd(), GetController()->GetBuffer())); + + EnterDataInPhase(); +} + +void ModePageDevice::ModeSelect(scsi_command, cdb_t, span, int) const +{ + // There is no default implementation of MODE SELECT + throw scsi_exception(sense_key::illegal_request, asc::invalid_command_operation_code); +} + +void ModePageDevice::ModeSelect6() const +{ + SaveParametersCheck(GetController()->GetCmdByte(4)); +} + +void ModePageDevice::ModeSelect10() const +{ + const auto length = min(GetController()->GetBuffer().size(), static_cast(GetInt16(GetController()->GetCmd(), 7))); + + SaveParametersCheck(static_cast(length)); +} + +void ModePageDevice::SaveParametersCheck(int length) const +{ + if (!SupportsSaveParameters() && (GetController()->GetCmdByte(1) & 0x01)) { + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } + + GetController()->SetLength(length); + + EnterDataOutPhase(); +} diff --git a/cpp/devices/mode_page_device.h b/cpp/devices/mode_page_device.h new file mode 100644 index 00000000..e905ce30 --- /dev/null +++ b/cpp/devices/mode_page_device.h @@ -0,0 +1,50 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "base/primary_device.h" +#include +#include +#include +#include + +class ModePageDevice : public PrimaryDevice +{ +public: + + using PrimaryDevice::PrimaryDevice; + + bool Init(const param_map&) override; + + virtual void ModeSelect(scsi_defs::scsi_command, cdb_t, span, int) const; + +protected: + + bool SupportsSaveParameters() const { return supports_save_parameters; } + void SupportsSaveParameters(bool b) { supports_save_parameters = b; } + int AddModePages(cdb_t, vector&, int, int, int) const; + virtual void SetUpModePages(map>&, int, bool) const = 0; + virtual void AddVendorPage(map>&, int, bool) const { + // Nothing to add by default + } + +private: + + bool supports_save_parameters = false; + + virtual int ModeSense6(cdb_t, vector&) const = 0; + virtual int ModeSense10(cdb_t, vector&) const = 0; + + void ModeSense6() const; + void ModeSense10() const; + void ModeSelect6() const; + void ModeSelect10() const; + + void SaveParametersCheck(int) const; +}; diff --git a/cpp/devices/optical_memory.cpp b/cpp/devices/optical_memory.cpp new file mode 100644 index 00000000..a6980031 --- /dev/null +++ b/cpp/devices/optical_memory.cpp @@ -0,0 +1,202 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +// Copyright (C) 2014-2020 GIMONS +// Coypright (C) akuker +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/shared_exceptions.h" +#include "base/scsi_command_util.h" +#include "optical_memory.h" + +using namespace scsi_command_util; + +OpticalMemory::OpticalMemory(int lun) : Disk(SCMO, lun, { 512, 1024, 2048, 4096 }) +{ + // 128 MB, 512 bytes per sector, 248826 sectors + geometries[512 * 248826] = { 512, 248826 }; + // 230 MB, 512 bytes per block, 446325 sectors + geometries[512 * 446325] = { 512, 446325 }; + // 540 MB, 512 bytes per sector, 1041500 sectors + geometries[512 * 1041500] = { 512, 1041500 }; + // 640 MB, 20248 bytes per sector, 310352 sectors + geometries[2048 * 310352] = { 2048, 310352 }; + + SetProtectable(true); + SetRemovable(true); + SetLockable(true); + + SupportsSaveParameters(true); +} + +void OpticalMemory::Open() +{ + assert(!IsReady()); + + // For some capacities there are hard-coded, well-defined sector sizes and block counts + if (const off_t size = GetFileSize(); !SetGeometryForCapacity(size)) { + // Sector size (default 512 bytes) and number of blocks + SetSectorSizeInBytes(GetConfiguredSectorSize() ? GetConfiguredSectorSize() : 512); + SetBlockCount(size >> GetSectorSizeShiftCount()); + } + + Disk::ValidateFile(); + + SetUpCache(0); + + // Attention if ready + if (IsReady()) { + SetAttn(true); + } +} + +vector OpticalMemory::InquiryInternal() const +{ + return HandleInquiry(device_type::optical_memory, scsi_level::scsi_2, true); +} + +void OpticalMemory::SetUpModePages(map>& pages, int page, bool changeable) const +{ + Disk::SetUpModePages(pages, page, changeable); + + // Page code 6 + if (page == 0x06 || page == 0x3f) { + AddOptionPage(pages, changeable); + } +} + +void OpticalMemory::AddFormatPage(map>& pages, bool changeable) const +{ + Disk::AddFormatPage(pages, changeable); + + EnrichFormatPage(pages, changeable, 1 << GetSectorSizeShiftCount()); +} + +void OpticalMemory::AddOptionPage(map>& pages, bool) const +{ + vector buf(4); + pages[6] = buf; + + // Do not report update blocks +} + +void OpticalMemory::ModeSelect(scsi_command cmd, cdb_t cdb, span buf, int length) const +{ + if (const string result = scsi_command_util::ModeSelect(cmd, cdb, buf, length, 1 << GetSectorSizeShiftCount()); + !result.empty()) { + LogWarn(result); + } +} + +// +// Mode page code 20h - Vendor Unique Format Page +// Format mode XXh type 0 +// Information: http://h20628.www2.hp.com/km-ext/kmcsdirect/emr_na-lpg28560-1.pdf + +// Offset description +// 02h format mode +// 03h type of format (0) +// 04~07h size of user band (total sectors?) +// 08~09h size of spare band (spare sectors?) +// 0A~0Bh number of bands +// +// Actual value of each 3.5 inches optical medium (grabbed by Fujitsu M2513EL) +// +// 128M 230M 540M 640M +// --------------------------------------------------- +// Size of user band 3CBFAh 6CF75h FE45Ch 4BC50h +// Size of spare band 0400h 0401h 08CAh 08C4h +// Number of bands 0001h 000Ah 0012h 000Bh +// +// Further information: http://r2089.blog36.fc2.com/blog-entry-177.html +// +void OpticalMemory::AddVendorPage(map>& pages, int page, bool changeable) const +{ + if (page != 0x20 && page != 0x3f) { + return; + } + + vector buf(12); + + // No changeable area + if (changeable) { + pages[32] = buf; + + return; + } + + if (IsReady()) { + unsigned spare = 0; + unsigned bands = 0; + uint64_t blocks = GetBlockCount(); + + if (GetSectorSizeInBytes() == 512) { + switch (blocks) { + // 128MB + case 248826: + spare = 1024; + bands = 1; + break; + + // 230MB + case 446325: + spare = 1025; + bands = 10; + break; + + // 540MB + case 1041500: + spare = 2250; + bands = 18; + break; + + default: + break; + } + } + + if (GetSectorSizeInBytes() == 2048) { + switch (blocks) { + // 640MB + case 310352: + spare = 2244; + bands = 11; + break; + + // 1.3GB (lpproj: not tested with real device) + case 605846: + spare = 4437; + bands = 18; + break; + + default: + break; + } + } + + buf[2] = (byte)0; // format mode + buf[3] = (byte)0; // type of format + SetInt32(buf, 4, static_cast(blocks)); + SetInt16(buf, 8, spare); + SetInt16(buf, 10, bands); + } + + pages[32] = buf; + + return; +} + +bool OpticalMemory::SetGeometryForCapacity(uint64_t capacity) { + if (const auto& geometry = geometries.find(capacity); geometry != geometries.end()) { + SetSectorSizeInBytes(geometry->second.first); + SetBlockCount(geometry->second.second); + + return true; + } + + return false; +} diff --git a/cpp/devices/optical_memory.h b/cpp/devices/optical_memory.h new file mode 100644 index 00000000..3d5b5971 --- /dev/null +++ b/cpp/devices/optical_memory.h @@ -0,0 +1,47 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +// Copyright (C) 2014-2020 GIMONS +// Copyright (C) akuker +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "disk.h" +#include +#include +#include + +using Geometry = pair; + +class OpticalMemory : public Disk +{ +public: + + explicit OpticalMemory(int); + ~OpticalMemory() override = default; + + void Open() override; + + vector InquiryInternal() const override; + void ModeSelect(scsi_defs::scsi_command, cdb_t, span, int) const override; + +protected: + + void SetUpModePages(map>&, int, bool) const override; + void AddFormatPage(map>&, bool) const override; + void AddVendorPage(map>&, int, bool) const override; + +private: + + void AddOptionPage(map>&, bool) const; + + bool SetGeometryForCapacity(uint64_t); + + // The mapping of supported capacities to block sizes and block counts, empty if there is no capacity restriction + unordered_map geometries; +}; diff --git a/cpp/devices/printer.cpp b/cpp/devices/printer.cpp new file mode 100644 index 00000000..50d385e0 --- /dev/null +++ b/cpp/devices/printer.cpp @@ -0,0 +1,234 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +// Implementation of a SCSI printer (see SCSI-2 specification for a command description) +// +//--------------------------------------------------------------------------- + +// +// How to print: +// +// 1. The client sends the data to be printed with one or several PRINT commands. The maximum +// transfer size per PRINT command is currently limited to 4096 bytes. +// 2. The client triggers printing with SYNCHRONIZE BUFFER. Each SYNCHRONIZE BUFFER results in +// the print command for this printer (see below) to be called for the data not yet printed. +// +// It is recommended to reserve the printer device before printing and to release it afterwards. +// The command to be used for printing can be set with the "cmd" property when attaching the device. +// By default the data to be printed are sent to the printer unmodified, using "lp -oraw %f". This +// requires that the client uses a printer driver compatible with the respective printer, or that the +// printing service on the Pi is configured to do any necessary conversions, or that the print command +// applies any conversions on the file to be printed (%f) before passing it to the printing service. +// 'enscript' is an example for a conversion tool. +// By attaching different devices/LUNs multiple printers (i.e. different print commands) are possible. +// +// With STOP PRINT printing can be cancelled before SYNCHRONIZE BUFFER was sent. +// + +#include "shared/shared_exceptions.h" +#include "base/scsi_command_util.h" +#include "printer.h" +#include + +using namespace std; +using namespace filesystem; +using namespace scsi_defs; +using namespace scsi_command_util; + +Printer::Printer(int lun) : PrimaryDevice(SCLP, lun) +{ + SupportsParams(true); +} + +bool Printer::Init(const param_map& params) +{ + PrimaryDevice::Init(params); + + AddCommand(scsi_command::cmd_test_unit_ready, [this] { TestUnitReady(); }); + AddCommand(scsi_command::cmd_print, [this] { Print(); }); + AddCommand(scsi_command::cmd_synchronize_buffer, [this] { Synchronize_buffer(); }); + // STOP PRINT is identical with TEST UNIT READY, it just returns the status + AddCommand(scsi_command::cmd_stop_print, [this] { TestUnitReady(); }); + + // Required also in this class in order to fulfill the ScsiPrinterCommands interface contract + AddCommand(scsi_command::cmd_reserve6, [this] { ReserveUnit(); }); + AddCommand(scsi_command::cmd_release6, [this] { ReleaseUnit(); }); + AddCommand(scsi_command::cmd_send_diagnostic, [this] { SendDiagnostic(); }); + + if (GetParam("cmd").find("%f") == string::npos) { + LogTrace("Missing filename specifier %f"); + return false; + } + + error_code error; + file_template = temp_directory_path(error); //NOSONAR Publicly writable directory is fine here + file_template += PRINTER_FILE_PATTERN; + + SetReady(true); + + return true; +} + +void Printer::CleanUp() +{ + PrimaryDevice::CleanUp(); + + if (out.is_open()) { + out.close(); + + error_code error; + filesystem::remove(path(filename), error); + + filename = ""; + } +} + +param_map Printer::GetDefaultParams() const +{ + return { + { "cmd", "lp -oraw %f" } + }; +} + +void Printer::TestUnitReady() +{ + // The printer is always ready + EnterStatusPhase(); +} + +vector Printer::InquiryInternal() const +{ + return HandleInquiry(device_type::printer, scsi_level::scsi_2, false); +} + +void Printer::Print() +{ + const uint32_t length = GetInt24(GetController()->GetCmd(), 2); + + LogTrace("Expecting to receive " + to_string(length) + " byte(s) to be printed"); + + if (length > GetController()->GetBuffer().size()) { + LogError("Transfer buffer overflow: Buffer size is " + to_string(GetController()->GetBuffer().size()) + + " bytes, " + to_string(length) + " bytes expected"); + + ++print_error_count; + + throw scsi_exception(sense_key::illegal_request, asc::invalid_field_in_cdb); + } + + GetController()->SetLength(length); + GetController()->SetByteTransfer(true); + + EnterDataOutPhase(); +} + +void Printer::Synchronize_buffer() +{ + if (!out.is_open()) { + LogWarn("Nothing to print"); + + ++print_warning_count; + + throw scsi_exception(sense_key::aborted_command); + } + + string cmd = GetParam("cmd"); + const size_t file_position = cmd.find("%f"); + assert(file_position != string::npos); + cmd.replace(file_position, 2, filename); + + error_code error; + LogTrace("Printing file '" + filename + "' with " + to_string(file_size(path(filename), error)) + " byte(s)"); + + LogDebug("Executing print command '" + cmd + "'"); + + if (system(cmd.c_str())) { + LogError("Printing file '" + filename + "' failed, the printing system might not be configured"); + + ++print_error_count; + + CleanUp(); + + throw scsi_exception(sense_key::aborted_command); + } + + CleanUp(); + + EnterStatusPhase(); +} + +bool Printer::WriteByteSequence(span buf) +{ + byte_receive_count += buf.size(); + + if (!out.is_open()) { + vector f(file_template.begin(), file_template.end()); + f.push_back(0); + + // There is no C++ API that generates a file with a unique name + const int fd = mkstemp(f.data()); + if (fd == -1) { + LogError("Can't create printer output file for pattern '" + filename + "': " + strerror(errno)); + ++print_error_count; + return false; + } + close(fd); + + filename = f.data(); + + out.open(filename, ios::binary); + if (out.fail()) { + ++print_error_count; + return false; + } + + LogTrace("Created printer output file '" + filename + "'"); + } + + LogTrace("Appending " + to_string(buf.size()) + " byte(s) to printer output file ''" + filename + "'"); + + out.write((const char *)buf.data(), buf.size()); + + const bool status = out.fail(); + if (status) { + ++print_error_count; + } + + return !status; +} + +vector Printer::GetStatistics() const +{ + vector statistics = PrimaryDevice::GetStatistics(); + + PbStatistics s; + s.set_id(GetId()); + s.set_unit(GetLun()); + + s.set_category(PbStatisticsCategory::CATEGORY_INFO); + + s.set_key(FILE_PRINT_COUNT); + s.set_value(file_print_count); + statistics.push_back(s); + + s.set_key(BYTE_RECEIVE_COUNT); + s.set_value(byte_receive_count); + statistics.push_back(s); + + s.set_category(PbStatisticsCategory::CATEGORY_ERROR); + + s.set_key(PRINT_ERROR_COUNT); + s.set_value(print_error_count); + statistics.push_back(s); + + s.set_category(PbStatisticsCategory::CATEGORY_WARNING); + + s.set_key(PRINT_WARNING_COUNT); + s.set_value(print_warning_count); + statistics.push_back(s); + + return statistics; +} diff --git a/cpp/devices/printer.h b/cpp/devices/printer.h new file mode 100644 index 00000000..55651498 --- /dev/null +++ b/cpp/devices/printer.h @@ -0,0 +1,67 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +// Implementation of a SCSI printer (see SCSI-2 specification for a command description) +// +//--------------------------------------------------------------------------- +#pragma once + +#include "base/interfaces/scsi_printer_commands.h" +#include "base/primary_device.h" +#include +#include +#include +#include + +using namespace std; + +class Printer : public PrimaryDevice, private ScsiPrinterCommands +{ + uint64_t file_print_count = 0; + uint64_t byte_receive_count = 0; + uint64_t print_error_count = 0; + uint64_t print_warning_count = 0; + + static const int NOT_RESERVED = -2; + + static constexpr const char *PRINTER_FILE_PATTERN = "/scsi2pi_sclp-XXXXXX"; + + inline static const string FILE_PRINT_COUNT = "file_print_count"; + inline static const string BYTE_RECEIVE_COUNT = "byte_receive_count"; + inline static const string PRINT_ERROR_COUNT = "print_error_count"; + inline static const string PRINT_WARNING_COUNT = "print_warning_count"; + +public: + + explicit Printer(int); + ~Printer() override = default; + + bool Init(const param_map&) override; + void CleanUp() override; + + param_map GetDefaultParams() const override; + + vector InquiryInternal() const override; + + bool WriteByteSequence(span) override; + + vector GetStatistics() const override; + +private: + + void TestUnitReady() override; + void ReserveUnit() override { PrimaryDevice::ReserveUnit(); } + void ReleaseUnit() override { PrimaryDevice::ReleaseUnit(); } + void SendDiagnostic() override { PrimaryDevice::SendDiagnostic(); } + void Print() override; + void Synchronize_buffer(); + + string file_template; + + string filename; + + ofstream out; +}; diff --git a/cpp/devices/sasi_hd.cpp b/cpp/devices/sasi_hd.cpp new file mode 100644 index 00000000..c783e779 --- /dev/null +++ b/cpp/devices/sasi_hd.cpp @@ -0,0 +1,64 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/scsi.h" +#include "disk.h" +#include "sasi_hd.h" + +SasiHd::SasiHd(int lun, const unordered_set& sector_sizes) : Disk(SAHD, lun, sector_sizes) +{ + SetProtectable(true); +} + +void SasiHd::FinalizeSetup(off_t image_offset) +{ + Disk::ValidateFile(); + + SetUpCache(image_offset); +} + +void SasiHd::Open() +{ + assert(!IsReady()); + + const off_t size = GetFileSize(); + + // Sector size (default 512 bytes) and number of blocks + SetSectorSizeInBytes(GetConfiguredSectorSize() ? GetConfiguredSectorSize() : 512); + SetBlockCount(static_cast(size >> GetSectorSizeShiftCount())); + + FinalizeSetup(0); +} + +void SasiHd::Inquiry() +{ + // Byte 0 = 0: Direct access device + + array buf = { }; + GetController()->CopyToBuffer(buf.data(), buf.size()); + + EnterDataInPhase(); +} + +vector SasiHd::InquiryInternal() const +{ + assert(false); + return vector(); +} + +void SasiHd::RequestSense() +{ + // Transfer 4 bytes when size is 0 (Shugart Associates System Interface specification) + //vector buf(allocation_length ? allocation_length : 4); + + // SASI fixed to non-extended format + array buf = { static_cast(GetStatusCode() >> 16), static_cast(GetLun() << 5) }; + GetController()->CopyToBuffer(buf.data(), buf.size()); + + EnterDataInPhase(); +} diff --git a/cpp/devices/sasi_hd.h b/cpp/devices/sasi_hd.h new file mode 100644 index 00000000..21628556 --- /dev/null +++ b/cpp/devices/sasi_hd.h @@ -0,0 +1,29 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "disk.h" +#include +#include + +class SasiHd : public Disk +{ + +public: + + explicit SasiHd(int, const unordered_set& = { 256, 512, 1024 }); + ~SasiHd() = default; + + void FinalizeSetup(off_t); + void Open() override; + + void Inquiry() override; + vector InquiryInternal() const override; + void RequestSense() override; +}; diff --git a/cpp/devices/scsi_hd.cpp b/cpp/devices/scsi_hd.cpp new file mode 100644 index 00000000..8bfdaf40 --- /dev/null +++ b/cpp/devices/scsi_hd.cpp @@ -0,0 +1,99 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/shared_exceptions.h" +#include "base/scsi_command_util.h" +#include "scsi_hd.h" + +using namespace scsi_command_util; + +ScsiHd::ScsiHd(int lun, bool removable, scsi_defs::scsi_level level, const unordered_set& sector_sizes) + : Disk(removable ? SCRM : SCHD, lun, sector_sizes), scsi_level(level) +{ + SetProtectable(true); + SetRemovable(removable); + SetLockable(removable); + + SupportsSaveParameters(true); +} + +string ScsiHd::GetProductData() const +{ + uint64_t capacity = GetBlockCount() * GetSectorSizeInBytes(); + string unit; + + // 10,000 MiB and more + if (capacity >= 10'485'760'000) { + capacity /= 1'073'741'824; + unit = "GiB"; + } + // 1 MiB and more + else if (capacity >= 1'048'576) { + capacity /= 1'048'576; + unit = "MiB"; + } + else { + capacity /= 1024; + unit = "KiB"; + } + + return DEFAULT_PRODUCT + " " + to_string(capacity) + " " + unit; +} + +void ScsiHd::FinalizeSetup(off_t image_offset) +{ + Disk::ValidateFile(); + + // For non-removable media drives set the default product name based on the drive capacity + if (!IsRemovable()) { + SetProduct(GetProductData(), false); + } + + SetUpCache(image_offset); +} + +void ScsiHd::Open() +{ + assert(!IsReady()); + + const off_t size = GetFileSize(); + + // Sector size (default 512 bytes) and number of blocks + SetSectorSizeInBytes(GetConfiguredSectorSize() ? GetConfiguredSectorSize() : 512); + SetBlockCount(static_cast(size >> GetSectorSizeShiftCount())); + + FinalizeSetup(0); +} + +vector ScsiHd::InquiryInternal() const +{ + return HandleInquiry(device_type::direct_access, scsi_level, IsRemovable()); +} + +void ScsiHd::ModeSelect(scsi_command cmd, cdb_t cdb, span buf, int length) const +{ + if (const string result = scsi_command_util::ModeSelect(cmd, cdb, buf, length, 1 << GetSectorSizeShiftCount()); + !result.empty()) { + LogWarn(result); + } +} + +void ScsiHd::AddFormatPage(map>& pages, bool changeable) const +{ + Disk::AddFormatPage(pages, changeable); + + EnrichFormatPage(pages, changeable, 1 << GetSectorSizeShiftCount()); +} + +void ScsiHd::AddVendorPage(map>& pages, int page, bool changeable) const +{ + // Page code 48 + if (page == 0x30 || page == 0x3f) { + AddAppleVendorModePage(pages, changeable); + } +} diff --git a/cpp/devices/scsi_hd.h b/cpp/devices/scsi_hd.h new file mode 100644 index 00000000..f0c0d6b9 --- /dev/null +++ b/cpp/devices/scsi_hd.h @@ -0,0 +1,43 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "disk.h" +#include +#include +#include +#include +#include + +class ScsiHd : public Disk +{ + const string DEFAULT_PRODUCT = "SCSI HD"; + +public: + + ScsiHd(int, bool, scsi_defs::scsi_level, const unordered_set& = { 512, 1024, 2048, 4096 }); + ~ScsiHd() override = default; + + void FinalizeSetup(off_t); + + void Open() override; + + // Commands + vector InquiryInternal() const override; + void ModeSelect(scsi_defs::scsi_command, cdb_t, span, int) const override; + + void AddFormatPage(map>&, bool) const override; + void AddVendorPage(map>&, int, bool) const override; + +private: + + string GetProductData() const; + + scsi_defs::scsi_level scsi_level; +}; diff --git a/cpp/devices/storage_device.cpp b/cpp/devices/storage_device.cpp new file mode 100644 index 00000000..ba0788c6 --- /dev/null +++ b/cpp/devices/storage_device.cpp @@ -0,0 +1,118 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/shared_exceptions.h" +#include "storage_device.h" +#include + +using namespace std; +using namespace filesystem; + +StorageDevice::StorageDevice(PbDeviceType type, int lun) : ModePageDevice(type, lun) +{ + SupportsFile(true); + SetStoppable(true); +} + +void StorageDevice::CleanUp() +{ + UnreserveFile(); + + ModePageDevice::CleanUp(); +} + +void StorageDevice::SetFilename(string_view f) +{ + filename = filesystem::path(f); + + // Permanently write-protected + SetReadOnly(IsReadOnlyFile()); + + SetProtectable(!IsReadOnlyFile()); + + if (IsReadOnlyFile()) { + SetProtected(false); + } +} + +void StorageDevice::ValidateFile() +{ + if (!blocks) { + throw io_exception(string(GetTypeString()) + " device has 0 blocks"); + } + + if (!exists(filename)) { + throw file_not_found_exception("Image file '" + filename.string() + "' for " + GetTypeString() + " device does not exist"); + } + + if (GetFileSize() > 2LL * 1024 * 1024 * 1024 * 1024) { + throw io_exception("Image files > 2 TiB are not supported"); + } + + // TODO Check for duplicate handling of these properties (-> S2pExecutor) + if (IsReadOnlyFile()) { + // Permanently write-protected + SetReadOnly(true); + SetProtectable(false); + SetProtected(false); + } + + SetStopped(false); + SetRemoved(false); + SetLocked(false); + SetReady(true); +} + +void StorageDevice::ReserveFile() const +{ + assert(!filename.empty()); + assert(!reserved_files.contains(filename.string())); + + reserved_files[filename.string()] = { GetId(), GetLun() }; +} + +void StorageDevice::UnreserveFile() +{ + reserved_files.erase(filename.string()); + + filename.clear(); +} + +id_set StorageDevice::GetIdsForReservedFile(const string& file) +{ + if (const auto& it = reserved_files.find(file); it != reserved_files.end()) { + return { it->second.first, it->second.second }; + } + + return { -1, -1 }; +} + +void StorageDevice::UnreserveAll() +{ + reserved_files.clear(); +} + +bool StorageDevice::FileExists(string_view file) +{ + return exists(path(file)); +} + +bool StorageDevice::IsReadOnlyFile() const +{ + return access(filename.c_str(), W_OK); +} + +off_t StorageDevice::GetFileSize() const +{ + try { + return file_size(filename); + } + catch (const filesystem_error& e) { + throw io_exception("Can't get size of '" + filename.string() + "': " + e.what()); + } +} diff --git a/cpp/devices/storage_device.h b/cpp/devices/storage_device.h new file mode 100644 index 00000000..263cc770 --- /dev/null +++ b/cpp/devices/storage_device.h @@ -0,0 +1,73 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +// The base class for all mass storage devices with image file support +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "shared/s2p_util.h" +#include "mode_page_device.h" +#include +#include +#include + +using namespace std; + +class StorageDevice : public ModePageDevice +{ +public: + + StorageDevice(PbDeviceType, int); + ~StorageDevice() override = default; + + void CleanUp() override; + + virtual void Open() = 0; + + string GetFilename() const { return filename.string(); } + void SetFilename(string_view); + + uint64_t GetBlockCount() const { return blocks; } + + void ReserveFile() const; + void UnreserveFile(); + // TODO Remove this method, it is only used by the unit tests + static void UnreserveAll(); + + static bool FileExists(string_view); + + void SetMediumChanged(bool b) { medium_changed = b; } + + static auto GetReservedFiles() { return reserved_files; } + static void SetReservedFiles(const unordered_map>& r) + { reserved_files = r; } + static id_set GetIdsForReservedFile(const string&); + +protected: + + void ValidateFile(); + + bool IsMediumChanged() const { return medium_changed; } + + void SetBlockCount(uint64_t b) { blocks = b; } + + off_t GetFileSize() const; + +private: + + bool IsReadOnlyFile() const; + + uint64_t blocks = 0; + + filesystem::path filename; + + bool medium_changed = false; + + // The list of image files in use and the IDs and LUNs using these files + static inline unordered_map> reserved_files; +}; diff --git a/cpp/doc/s2p.1 b/cpp/doc/s2p.1 new file mode 100644 index 00000000..d342ebb2 --- /dev/null +++ b/cpp/doc/s2p.1 @@ -0,0 +1,114 @@ +.TH s2p 1 +.SH NAME +s2p \- Emulates SCSI devices using the Raspberry Pi GPIO pins +.SH SYNOPSIS +.B s2p +[\fB\-F\fR \fIFOLDER\fR] +[\fB\-L\fR \fILOG_LEVEL[:ID:[LUN]]\fR] +[\fB\-P\fR \fIACCESS_TOKEN_FILE\fR] +[\fB\-R\fR \fISCAN_DEPTH\fR] +[\fB\-h\fR] +[\fB\-n\fR \fIVENDOR:PRODUCT:REVISION\fR] +[\fB\-p\fR \fIPORT\fR] +[\fB\-r\fR \fIRESERVED_IDS\fR] +[\fB\-n\fR \fITYPE\fR] +[\fB\-v\fR] +[\fB\-z\fR \fILOCALE\fR] +[\fB\-IDn:[u]\fR \fIFILE\fR] +[\fB\-HDn[:u]\fR \fIFILE\fR]... +.SH DESCRIPTION +.B s2p +emulates SCSI devices using the Raspberry Pi GPIO pins. +.PP +In the arguments to SCSI2Pi, one or more SCSI (-IDn[:u]) devices can be specified. +The number (n) after the ID or HD identifier specifies the ID number for that device. The optional number (u) specifies the LUN (logical unit) for that device. The default LUN is 0. +For SCSI: The ID is limited from 0-7. However, typically SCSI ID 7 is reserved for the "initiator" (the host computer). The LUN is limited from 0-31. +.PP +SCSI2Pi will determine the type of device based upon the file extension of the FILE argument. + hd1: SCSI Hard Disk image (generic, non-removable, SCSI-1) + hds: SCSI Hard Disk image (generic, non-removable) + hdr: SCSI Hard Disk image (generic, removable) + hdn: SCSI Hard Disk image (NEC compatible - only used with PC-98 computers) + hdi: SCSI Hard Disk image (Anex86 proprietary - only used with PC-98 computers) + nhd: SCSI Hard Disk image (T98Next proprietary - only used with PC-98 computers) + hda: SCSI Hard Disk image (Apple compatible - typically used with Macintosh computers) + mos: SCSI Magneto-Optical image (generic - typically used with NeXT, X68000, etc.) + iso: SCSI CD-ROM or DVD-ROM image (ISO 9660 image) + is1: SCSI CD-ROM or DVD-ROM image (ISO 9660 image, SCSI-1) + +For example, if you want to specify an Apple-compatible HD image on ID 0, you can use the following command: + sudo s2p -ID0 /path/to/drive/hdimage.hda + +Note: SCSI2Pi is a fork of RaSCSI. The two cannot be run in parallel on the same system. + +Once SCSI2Pi starts, it will open a socket (default port is 6868) to allow external management commands. +If another process is using this port, SCSI2Pi will terminate, since it is likely another instance of SCSI2Pi or RaSCSI. +Once SCSI2Pi has initialized, the scsictl utility can be used to send commands. + +To quit SCSI2Pi, press Control + C. If it is running in the background, you can kill it using an INT signal. + +.SH OPTIONS +.TP +.BR \-b\fI " " \fIBLOCK_SIZE +The optional block size, either 512, 1024, 2048 or 4096 bytes. Default size is 512 bytes. +.TP +.BR \-F\fI " " \fIFOLDER +The default folder for image files. For files in this folder no absolute path needs to be specified. The initial default folder is '~/images'. +.TP +.BR \-L\fI " " \fILOG_LEVEL[:ID:[LUN]] +The s2p log level (trace, debug, info, warning, error, off). The default log level is 'info' for all devices unless a particular device ID and an optional LUN was provided. +.TP +.BR \-P\fI " " \fIACCESS_TOKEN_FILE +Enable authentication and read the access token from the specified file. The access token file must be owned by root and must be readable by root only. +.TP +.BR \-R\fI " " \fISCAN_DEPTH +Scan for image files recursively, up to a depth of SCAN_DEPTH. Depth 0 means to ignore any folders within the default image filder. Be careful when using this option with many sub-folders in the default image folder. The default depth is 1. +.TP +.BR \-h\fI " " \fI +Show a help page. +.TP +.BR \-n\fI " " \fIVENDOR:PRODUCT:REVISION +Set the vendor, product and revision for the device, to be returned with the INQUIRY data. A complete set of name components must be provided. VENDOR may have up to 8, PRODUCT up to 16, REVISION up to 4 characters. Padding with blanks to the maxium length is automatically applied. Once set the name of a device cannot be changed. +.TP +.BR \-p\fI " " \fIPORT +The s2p server port, default is 6868. +.TP +.BR \-r\fI " " \fIRESERVED_IDS +Comma-separated list of IDs to reserve. Pass an empty list in order to not reserve anything. +.BR \-p\fI " " \fITYPE +The optional case-insensitive device type (SAHD, SCHD, SCRM, SCCD, SCMO, SCBR, SCDP, SCLP, SCHS). If no type is specified for devices that support an image file, s2p tries to derive the type from the file extension. +.TP +.BR \-v\fI " " \fI +Display the s2p version. +.TP +.BR \-z\fI " "\fILOCALE +Overrides the default locale for client-faces error messages. The client can override the locale. +.TP +.BR \-ID\fIn[:u] " " \fIFILE +n is the SCSI ID number (0-7). u (0-31) is the optional LUN (logical unit). The default LUN is 0. +.IP +FILE is the name of the image file to use for the SCSI device. For devices that do not support an image file (SCBR, SCDP, SCLP, SCHS) the filename may have a special meaning or a dummy name can be provided. For SCBR and SCDP it is an optioinal prioritized list of network interfaces, an optional IP address and netmask, e.g. "interface=eth0,eth1,wlan0:inet=10.10.20.1/24". For SCLP it is the print command to be used and a reservation timeout in seconds, e.g. "cmd=lp -oraw %f:timeout=60". +.IP +FILE is the name of the image file to use for the SCSI device. +.IP + +.SH EXAMPLES +Launch SCSI2Pi with no emulated drives attached: + s2p + +Launch SCSI2Pi with an Apple hard drive image as ID 0 and a CD-ROM as ID 2 + s2p -ID0 /path/to/harddrive.hda -ID2 /path/to/cdimage.iso + +Launch SCSI2Pi with a removable SCSI drive image as ID 0 and the raw device file /dev/hdb (e.g. a USB stick) and a DaynaPort network adapter as ID 6: + s2p -ID0 -t scrm /dev/hdb -ID6 -t scdp daynaport + +To create an empty, 100MiB HD image, use the following command: + dd if=/dev/zero of=/path/to/newimage.hda bs=512 count=204800 + +In case the fallocate command is available a much faster alternative to the dd command is: + fallocate -l 104857600 /path/to/newimage.hda + +.SH SEE ALSO +scsictl(1), scsiexec(1), scsidump(1) + +Full documentation is available at: diff --git a/cpp/doc/s2p_man_page.txt b/cpp/doc/s2p_man_page.txt new file mode 100644 index 00000000..9a013484 --- /dev/null +++ b/cpp/doc/s2p_man_page.txt @@ -0,0 +1,149 @@ +s2p(1) General Commands Manual s2p(1) + +NAME + s2p - Emulates SCSI devices using the Raspberry Pi GPIO pins + +SYNOPSIS + s2p [-F FOLDER] [-L LOG_LEVEL[:ID:[LUN]]] [-P ACCESS_TOKEN_FILE] [-R + SCAN_DEPTH] [-h] [-n VENDOR:PRODUCT:REVISION] [-p PORT] [-r RE‐ + SERVED_IDS] [-n TYPE] [-v] [-z LOCALE] [-IDn:[u] FILE] [-HDn[:u] + FILE]... + +DESCRIPTION + s2p emulates SCSI devices using the Raspberry Pi GPIO pins. + + In the arguments to SCSI2Pi, one or more SCSI (-IDn[:u]) devices can be + specified. The number (n) after the ID or HD identifier specifies the + ID number for that device. The optional number (u) specifies the LUN + (logical unit) for that device. The default LUN is 0. For SCSI: The ID + is limited from 0-7. However, typically SCSI ID 7 is reserved for the + "initiator" (the host computer). The LUN is limited from 0-31. + + SCSI2Pi will determine the type of device based upon the file extension + of the FILE argument. + hd1: SCSI Hard Disk image (generic, non-removable, SCSI-1) + hds: SCSI Hard Disk image (generic, non-removable) + hdr: SCSI Hard Disk image (generic, removable) + hdn: SCSI Hard Disk image (NEC compatible - only used with PC-98 + computers) + hdi: SCSI Hard Disk image (Anex86 proprietary - only used with + PC-98 computers) + nhd: SCSI Hard Disk image (T98Next proprietary - only used with + PC-98 computers) + hda: SCSI Hard Disk image (Apple compatible - typically used with + Macintosh computers) + mos: SCSI Magneto-Optical image (generic - typically used with + NeXT, X68000, etc.) + iso: SCSI CD-ROM or DVD-ROM image (ISO 9660 image) + is1: SCSI CD-ROM or DVD-ROM image (ISO 9660 image, SCSI-1) + + For example, if you want to specify an Apple-compatible HD image on ID + 0, you can use the following command: + sudo s2p -ID0 /path/to/drive/hdimage.hda + + Note: SCSI2Pi is a fork of RaSCSI. The two cannot be run in parallel on + the same system. + + Once SCSI2Pi starts, it will open a socket (default port is 6868) to + allow external management commands. If another process is using this + port, SCSI2Pi will terminate, since it is likely another instance of + SCSI2Pi or RaSCSI. Once SCSI2Pi has initialized, the scsictl utility + can be used to send commands. + + To quit SCSI2Pi, press Control + C. If it is running in the background, + you can kill it using an INT signal. + +OPTIONS + -b BLOCK_SIZE + The optional block size, either 512, 1024, 2048 or 4096 bytes. + Default size is 512 bytes. + + -F FOLDER + The default folder for image files. For files in this folder no + absolute path needs to be specified. The initial default folder + is '~/images'. + + -L LOG_LEVEL[:ID:[LUN]] + The s2p log level (trace, debug, info, warning, error, off). The + default log level is 'info' for all devices unless a particular + device ID and an optional LUN was provided. + + -P ACCESS_TOKEN_FILE + Enable authentication and read the access token from the speci‐ + fied file. The access token file must be owned by root and must + be readable by root only. + + -R SCAN_DEPTH + Scan for image files recursively, up to a depth of SCAN_DEPTH. + Depth 0 means to ignore any folders within the default image + filder. Be careful when using this option with many sub-folders + in the default image folder. The default depth is 1. + + -h Show a help page. + + -n VENDOR:PRODUCT:REVISION + Set the vendor, product and revision for the device, to be re‐ + turned with the INQUIRY data. A complete set of name components + must be provided. VENDOR may have up to 8, PRODUCT up to 16, RE‐ + VISION up to 4 characters. Padding with blanks to the maxium + length is automatically applied. Once set the name of a device + cannot be changed. + + -p PORT + The s2p server port, default is 6868. + + -r RESERVED_IDS + Comma-separated list of IDs to reserve. Pass an empty list in + order to not reserve anything. -p TYPE The optional case-insen‐ + sitive device type (SAHD, SCHD, SCRM, SCCD, SCMO, SCBR, SCDP, + SCLP, SCHS). If no type is specified for devices that support an + image file, s2p tries to derive the type from the file exten‐ + sion. + + -v Display the s2p version. + + -z LOCALE + Overrides the default locale for client-faces error messages. + The client can override the locale. + + -IDn[:u] FILE + n is the SCSI ID number (0-7). u (0-31) is the optional LUN + (logical unit). The default LUN is 0. + + FILE is the name of the image file to use for the SCSI device. + For devices that do not support an image file (SCBR, SCDP, SCLP, + SCHS) the filename may have a special meaning or a dummy name + can be provided. For SCBR and SCDP it is an optioinal priori‐ + tized list of network interfaces, an optional IP address and + netmask, e.g. "interface=eth0,eth1,wlan0:inet=10.10.20.1/24". + For SCLP it is the print command to be used and a reservation + timeout in seconds, e.g. "cmd=lp -oraw %f:timeout=60". + + FILE is the name of the image file to use for the SCSI device. + +EXAMPLES + Launch SCSI2Pi with no emulated drives attached: + s2p + + Launch SCSI2Pi with an Apple hard drive image as ID 0 and a CD-ROM as + ID 2 + s2p -ID0 /path/to/harddrive.hda -ID2 /path/to/cdimage.iso + + Launch SCSI2Pi with a removable SCSI drive image as ID 0 and the raw + device file /dev/hdb (e.g. a USB stick) and a DaynaPort network adapter + as ID 6: + s2p -ID0 -t scrm /dev/hdb -ID6 -t scdp daynaport + + To create an empty, 100MiB HD image, use the following command: + dd if=/dev/zero of=/path/to/newimage.hda bs=512 count=204800 + + In case the fallocate command is available a much faster alternative to + the dd command is: + fallocate -l 104857600 /path/to/newimage.hda + +SEE ALSO + scsictl(1), scsiexec(1), scsidump(1) + + Full documentation is available at: + + s2p(1) diff --git a/cpp/doc/s2pctl.1 b/cpp/doc/s2pctl.1 new file mode 100644 index 00000000..24891750 --- /dev/null +++ b/cpp/doc/s2pctl.1 @@ -0,0 +1,195 @@ +.TH s2p 1 +.SH NAME +scsictl \- Sends management commands to the s2p process +.SH SYNOPSIS +.B scsictl +\fB\-e\fR | +\fB\-l\fR | +\fB\-m\fR | +\fB\-o\fR | +\fB\-v\fR | +\fB\-D\fR | +\fB\-I\fR | +\fB\-L\fR | +\fB\-O\fR | +\fB\-P\fR | +\fB\-S\fR | +\fB\-T\fR | +\fB\-V\fR | +\fB\-X\fR | +\fB\-Z\fR | +[\fB\-d\fR \fIFILENAME\fR] | +[\fB\-B\fR \fIFILENAME\fR] | +[\fB\-J\fR \fIFILENAME\fR] | +[\fB\-C\fR \fIFILENAME:FILESIZE\fR] | +[\fB\-E\fR \fIFILENAME\fR] | +[\fB\-F\fR \fIIMAGE_FOLDER\fR] | +[\fB\-R\fR \fICURRENT_NAME:NEW_NAME\fR] | +[\fB\-c\fR \fICMD\fR] | +[\fB\-f\fR \fIFILE|PARAM\fR] | +[\fB\-L\fR \fILOG_LEVEL\fR] | +[\fB\-h\fR \fIHOST\fR] | +[\fB\-i\fR \fIID[:LUN]\fR] | +[\fB\-n\fR \fINAME\fR] | +[\fB\-p\fR \fIPORT\fR] | +[\fB\-r\fR \fIRESERVED_IDS\fR] | +[\fB\-s\fR \fI[FOLDER_PATTERN:FILE_PATTERN:OPERATIONS]\fR] | +[\fB\-t\fR \fITYPE\fR] | +[\fB\-x\fR \fICURRENT_NAME:NEW_NAME\fR] | +[\fB\-z\fR \fILOCALE\fR] +.SH DESCRIPTION +.B scsictl +sends commands to the s2p process to make configuration adjustments at runtime or to check the status of the devices. + +Either the -i or -l option should be specified at one time. Not both. + +You do NOT need root privileges to use scsictl. scsictl also runs on non-Pi Linux platforms. + +Note: The command and type arguments are case insensitive. Only the first letter of the command/type is evaluated by the tool. + +.SH OPTIONS +.TP +.BR \-C\fI " "\fIFILENAME:FILESIZE +Create an image file in the default image folder with the specified name and size in bytes. +.TP +.BR \-D\fI +Detach all devices. +.TP +.BR \-E\fI " " \fIFILENAME +Display information on an image file. +.TP +.BR \-F\fI " "\fIIMAGE_FOLDER +Set the default image folder. +.TP +.BR \-I\fI +Gets the list of reserved device IDs. +.TP +.BR \-L\fI " "\fILOG_LEVEL +Set the s2p log level (trace, debug, info, warning, error, off). +.TP +.BR \-h\fI " " \fIHOST +The s2p host to connect to, default is 'localhost'. +.TP +.BR \-e\fI +List all images files in the default image folder. +.TP +.BR \-N\fI +Lists all available network interfaces provided that they are up. +.TP +.BR \-O\fI +Display the available s2p server log levels and the current log level. +.TP +.BR \-P\fI +Prompt for the access token in case s2p requires authentication. +.TP +.BR \-l\fI +List all of the devices that are currently being emulated by SCSI2Pi, as well as their current status. +.TP +.BR \-m\fI +List all file extensions recognized by SCSI2Pi and the device types they map to. +.TP +.BR \-o\fI +Display operation meta data information. +.TP +.BR \-R\fI " "\fICURRENT_NAME:NEW_NAME +Rename an image file in the default image folder. +.TP +.BR \-p\fI " " \fIPORT +The s2p port to connect to, default is 6868. +.TP +.BR \-r\fI " " \fIRESERVED_IDS +Comma-separated list of IDs to reserve. Pass an empty list in order to not reserve anything. +.TP +.BR \-s\fI " " \fI[FOLDER_PATTERN:FILE_PATTERN:OPERATIONS] +Display server-side settings like available images or supported device types. +.TP +.BR \-S\fI +Display statistics. +.TP +.BR \-T\fI +Display all device types and their properties. +.TP +.BR \-v\fI " " \fI +Display the s2p server version. +.TP +.BR \-V\fI " " \fI +Display the scsictl version. +.TP +.BR \-X\fI " " \fI +Shut down the s2p process. +.TP +.BR \-d\fI " "\fIFILENAME +Delete an image file in the default image folder. +.TP +.BR \-B\fI " "\fIFILENAME +Do not send command to s2p but write it to a protobuf binary file. +.TP +.BR \-J\fI " "\fIFILENAME +Do not send command to s2p but write it to a protobuf JSON file. +.TP +.BR \-Z\fI " "\fIFILENAME +Do not send command to s2p but write it to a protobuf text format file. +.TP +.BR \-x\fI " "\fICURRENT_NAME:NEW_NAME +Copy an image file in the default image folder. +.TP +.BR \-z\fI " "\fILOCALE +Overrides the default locale for client-facing error messages. +.TP +.BR \-i\fI " " \fIID[:LUN] +The SCSI ID and optional LUN that you want to control. (0-7:0-31) +.TP +.BR \-c\fI " " \fICMD +Command is the operation being requested. Options are: + a(ttach): Attach disk + d(etach): Detach disk + i(nsert): Insert media (removable media devices only) + e(ject): Eject media (removable media devices only) + p(rotect): Write protect the medium (not for CD-ROMs, which are always read-only) + u(nprotect): Remove write protection from the medium (not for CD-ROMs, which are always read-only) + s(how): Display device information +.IP +eject, protect and unprotect are idempotent. +.TP +.BR \-b\fI " " \fIBLOCK_SIZE +The optional block size, either 512, 1024, 2048 or 4096 bytes. The default size is 512 bytes. +.TP +.BR \-f\fI " " \fIFILE|PARAM +Device-specific: Either a path to a disk image file, or a parameter for a non-disk device. See the s2p(1) man page for permitted file types. +.TP +.BR \-t\fI " " \fITYPE +Specifies the device type. This type overrides the type derived from the file extension of the specified image. See the s2p(1) man page for the available device types. For some types there are shortcuts (only the first letter is required): + hd: SCSI hard disk drive + rm: SCSI removable media drive + cd: CD-ROM + mo: Magneto-Optical disk + lp: SCSI printer + bridge: Bridge device (Only applicable to the Sharp X68000) + daynaport: DaynaPort network adapter + services: Host services device +.TP +.BR \-n\fI " " \fIVENDOR:PRODUCT:REVISION +The vendor, product and revision for the device, to be returned with the INQUIRY data. A complete set of name components must be provided. VENDOR may have up to 8, PRODUCT up to 16, REVISION up to 4 characters. Padding with blanks to the maxium length is automatically applied. Once set the name of a device cannot be changed. +.TP +.BR \-u\fI " " \fIUNIT +Unit number (0-31). This will default to 0. This option is only used when there are multiple SCSI devices on a shared SCSI controller. (This is not common) + +.SH EXAMPLES +Show a listing of all of the SCSI devices and their current status. + scsictl -l + + +Example output: + +----+-----+------+------------------------------------- + | ID | LUN | TYPE | IMAGE FILE + +----+-----+------+------------------------------------- + | 0 | 1 | SCHD | /home/pi/harddisk.hda + +----+-----+------+------------------------------------- + +Request the SCSI2Pi process to attach a disk (assumed) to SCSI ID 0 with the contents of the file system image "HDIIMAGE0.HDS". + scsictl -i 0 -f HDIIMAGE0.HDS + +.SH SEE ALSO +s2p(1), scsiexec(1), scsidump(1) + +Full documentation is available at: diff --git a/cpp/doc/s2pctl_man_page.txt b/cpp/doc/s2pctl_man_page.txt new file mode 100644 index 00000000..cd00e47b --- /dev/null +++ b/cpp/doc/s2pctl_man_page.txt @@ -0,0 +1,183 @@ +s2p(1) General Commands Manual s2p(1) + +NAME + scsictl - Sends management commands to the s2p process + +SYNOPSIS + scsictl -e | -l | -m | -o | -v | -D | -I | -L | -O | -P | -S | -T | -V + | -X | -Z | [-d FILENAME] | [-B FILENAME] | [-J FILENAME] | [-C FILE‐ + NAME:FILESIZE] | [-E FILENAME] | [-F IMAGE_FOLDER] | [-R CUR‐ + RENT_NAME:NEW_NAME] | [-c CMD] | [-f FILE|PARAM] | [-L LOG_LEVEL] | [-h + HOST] | [-i ID[:LUN]] | [-n NAME] | [-p PORT] | [-r RESERVED_IDS] | [-s + [FOLDER_PATTERN:FILE_PATTERN:OPERATIONS]] | [-t TYPE] | [-x CUR‐ + RENT_NAME:NEW_NAME] | [-z LOCALE] + +DESCRIPTION + scsictl sends commands to the s2p process to make configuration adjust‐ + ments at runtime or to check the status of the devices. + + Either the -i or -l option should be specified at one time. Not both. + + You do NOT need root privileges to use scsictl. scsictl also runs on + non-Pi Linux platforms. + + Note: The command and type arguments are case insensitive. Only the + first letter of the command/type is evaluated by the tool. + +OPTIONS + -C FILENAME:FILESIZE + Create an image file in the default image folder with the speci‐ + fied name and size in bytes. + + -D Detach all devices. + + -E FILENAME + Display information on an image file. + + -F IMAGE_FOLDER + Set the default image folder. + + -I Gets the list of reserved device IDs. + + -L LOG_LEVEL + Set the s2p log level (trace, debug, info, warning, error, off). + + -h HOST + The s2p host to connect to, default is 'localhost'. + + -e List all images files in the default image folder. + + -N Lists all available network interfaces provided that they are + up. + + -O Display the available s2p server log levels and the current log + level. + + -P Prompt for the access token in case s2p requires authentication. + + -l List all of the devices that are currently being emulated by + SCSI2Pi, as well as their current status. + + -m List all file extensions recognized by SCSI2Pi and the device + types they map to. + + -o Display operation meta data information. + + -R CURRENT_NAME:NEW_NAME + Rename an image file in the default image folder. + + -p PORT + The s2p port to connect to, default is 6868. + + -r RESERVED_IDS + Comma-separated list of IDs to reserve. Pass an empty list in + order to not reserve anything. + + -s [FOLDER_PATTERN:FILE_PATTERN:OPERATIONS] + Display server-side settings like available images or supported + device types. + + -S Display statistics. + + -T Display all device types and their properties. + + -v Display the s2p server version. + + -V Display the scsictl version. + + -X Shut down the s2p process. + + -d FILENAME + Delete an image file in the default image folder. + + -B FILENAME + Do not send command to s2p but write it to a protobuf binary + file. + + -J FILENAME + Do not send command to s2p but write it to a protobuf JSON file. + + -Z FILENAME + Do not send command to s2p but write it to a protobuf text for‐ + mat file. + + -x CURRENT_NAME:NEW_NAME + Copy an image file in the default image folder. + + -z LOCALE + Overrides the default locale for client-facing error messages. + + -i ID[:LUN] + The SCSI ID and optional LUN that you want to control. + (0-7:0-31) + + -c CMD Command is the operation being requested. Options are: + a(ttach): Attach disk + d(etach): Detach disk + i(nsert): Insert media (removable media devices only) + e(ject): Eject media (removable media devices only) + p(rotect): Write protect the medium (not for CD-ROMs, which + are always read-only) + u(nprotect): Remove write protection from the medium (not for + CD-ROMs, which are always read-only) + s(how): Display device information + + eject, protect and unprotect are idempotent. + + -b BLOCK_SIZE + The optional block size, either 512, 1024, 2048 or 4096 bytes. + The default size is 512 bytes. + + -f FILE|PARAM + Device-specific: Either a path to a disk image file, or a param‐ + eter for a non-disk device. See the s2p(1) man page for permit‐ + ted file types. + + -t TYPE + Specifies the device type. This type overrides the type derived + from the file extension of the specified image. See the s2p(1) + man page for the available device types. For some types there + are shortcuts (only the first letter is required): + hd: SCSI hard disk drive + rm: SCSI removable media drive + cd: CD-ROM + mo: Magneto-Optical disk + lp: SCSI printer + bridge: Bridge device (Only applicable to the Sharp X68000) + daynaport: DaynaPort network adapter + services: Host services device + + -n VENDOR:PRODUCT:REVISION + The vendor, product and revision for the device, to be returned + with the INQUIRY data. A complete set of name components must be + provided. VENDOR may have up to 8, PRODUCT up to 16, REVISION up + to 4 characters. Padding with blanks to the maxium length is au‐ + tomatically applied. Once set the name of a device cannot be + changed. + + -u UNIT + Unit number (0-31). This will default to 0. This option is only + used when there are multiple SCSI devices on a shared SCSI con‐ + troller. (This is not common) + +EXAMPLES + Show a listing of all of the SCSI devices and their current status. + scsictl -l + + Example output: + +----+-----+------+------------------------------------- + | ID | LUN | TYPE | IMAGE FILE + +----+-----+------+------------------------------------- + | 0 | 1 | SCHD | /home/pi/harddisk.hda + +----+-----+------+------------------------------------- + + Request the SCSI2Pi process to attach a disk (assumed) to SCSI ID 0 + with the contents of the file system image "HDIIMAGE0.HDS". + scsictl -i 0 -f HDIIMAGE0.HDS + +SEE ALSO + s2p(1), scsiexec(1), scsidump(1) + + Full documentation is available at: + + s2p(1) diff --git a/cpp/doc/s2pdump.1 b/cpp/doc/s2pdump.1 new file mode 100644 index 00000000..9bd8d2f5 --- /dev/null +++ b/cpp/doc/s2pdump.1 @@ -0,0 +1,71 @@ +.TH scsidump 1 +.SH NAME +scsidump \- SCSI disk dumping tool for SCSI2Pi +.SH SYNOPSIS +.B scsidump +\fB\-t\fR \fIID[:LUN]\fR +[\fB\-i\fR \fIBID\fR] +\fB\-f\fR \fIFILE\fR +[\fB\-s\fR \fIBUFFER_SIZE\fR] +[\fB\-r\fR] +[\fB\-v\fR] +[\fB\-p\fR] +[\fB\-I\fR] ID[:LUN] +[\fB\-S\fR] +.SH DESCRIPTION +.B scsidump +has two modes of operation: dump and restore. These can be used with physical storage media, including hard drives and magneto optical drives. Dump mode can be used with read-only media such as CD/DVD drives. + +When operating in dump mode, scsidump will copy all data from a remote SCSI drive to an image on the local filesystem. If enabled, it will also generate a .properties file that can be used to more closely emulate the source drive. + +If you are operating in restore mode, scsidump will copy the data from a local binary image to a remote physical SCSI drive. The remote SCSI drive MUST be writable. + +.SH NOTES + +.B scsidump +requires either a direct connection (one without transceivers) or a FULLSPEC SCSI2Pi/RaSCSI board. + +If the generated drive image is intended to be used with SCSI2Pi, the drive image should be moved by the user to ~/images (or the location specified to the s2p service). + +.SH OPTIONS +.TP +.BR \-t\fI " "\fIID[:LUN] +SCSI ID and optional LUN of the remote SCSI device. The remote SCSI device will be functioning as the "Target" device. +.TP +.BR \-i\fI " "\fIBID +SCSI ID of the SCSI2Pi device. If not specified, the SCSI2Pi device will use ID 7. The SCSI2Pi host will be functioning as the "Initiator" device. +.TP +.BR \-f\fI " "\fIFILE +Path to the dump file. +.TP +.BR \-s\fI " "\fIBUFFER_SIZE +The transfer buffer size, specified in bytes. Default is 1 MiB. This is specified in bytes with a minimum value of 65536 (64 KiB). +.TP +.BR \-r\fI +Run in restore mode. Defaults to dump mode if not specified. +.TP +.BR \-v\fI +Enable verbose logging. +.TP +.BR \-p\fI +Generate a .properties file that is compatible with the SCSI2Pi web interface. The output filename will match the image filename with ".properties" appended. The generated file should be moved to ~/.config/s2p. +.TP +.BR \-I\fI " "\fIID[:LUN] +Display INQUIRY data of ID[:LUN]. +.TP +.BR \-S\fI +Scan SCSI bus for devices. + +.SH EXAMPLES +Dump Mode: [SCSI Drive] ---> [SCSI2Pi host] +Launch scsidump to dump an all data from SCSI ID 3 with block size 64 KiB, store it to the local filesystem as a drive image named outimage.hda, and generate the outimage.hda.properties file with the drive's INQUIRY information: + scsidump -t 3 -f ./outimage.hda -s 65536 -p + +Restore Mode: [SCSI2Pi host] ---> [SCSI Drive] +Launch scsidump to restore/upload a drive image from the local file system to SCSI ID 0 with block size 1MiB: + scsidump -r -t 0 -f ./outimage.hda -s 1048576 + +.SH SEE ALSO +scsictl(1), s2p(1), scsiexec(1) + +Full documentation is available at: diff --git a/cpp/doc/s2pdump_man_page.txt b/cpp/doc/s2pdump_man_page.txt new file mode 100644 index 00000000..06f43aff --- /dev/null +++ b/cpp/doc/s2pdump_man_page.txt @@ -0,0 +1,81 @@ +scsidump(1) General Commands Manual scsidump(1) + +NAME + scsidump - SCSI disk dumping tool for SCSI2Pi + +SYNOPSIS + scsidump -t ID[:LUN] [-i BID] -f FILE [-s BUFFER_SIZE] [-r] [-v] [-p] + [-I] ID[:LUN] [-S] + +DESCRIPTION + scsidump has two modes of operation: dump and restore. These can be + used with physical storage media, including hard drives and magneto op‐ + tical drives. Dump mode can be used with read-only media such as CD/DVD + drives. + + When operating in dump mode, scsidump will copy all data from a remote + SCSI drive to an image on the local filesystem. If enabled, it will + also generate a .properties file that can be used to more closely emu‐ + late the source drive. + + If you are operating in restore mode, scsidump will copy the data from + a local binary image to a remote physical SCSI drive. The remote SCSI + drive MUST be writable. + +NOTES + scsidump requires either a direct connection (one without transceivers) + or a FULLSPEC SCSI2Pi/RaSCSI board. + + If the generated drive image is intended to be used with SCSI2Pi, the + drive image should be moved by the user to ~/images (or the location + specified to the s2p service). + +OPTIONS + -t ID[:LUN] + SCSI ID and optional LUN of the remote SCSI device. The remote + SCSI device will be functioning as the "Target" device. + + -i BID SCSI ID of the SCSI2Pi device. If not specified, the SCSI2Pi de‐ + vice will use ID 7. The SCSI2Pi host will be functioning as the + "Initiator" device. + + -f FILE + Path to the dump file. + + -s BUFFER_SIZE + The transfer buffer size, specified in bytes. Default is 1 MiB. + This is specified in bytes with a minimum value of 65536 (64 + KiB). + + -r Run in restore mode. Defaults to dump mode if not specified. + + -v Enable verbose logging. + + -p Generate a .properties file that is compatible with the SCSI2Pi + web interface. The output filename will match the image filename + with ".properties" appended. The generated file should be moved + to ~/.config/s2p. + + -I ID[:LUN] + Display INQUIRY data of ID[:LUN]. + + -S Scan SCSI bus for devices. + +EXAMPLES + Dump Mode: [SCSI Drive] ---> [SCSI2Pi host] Launch scsidump to dump an + all data from SCSI ID 3 with block size 64 KiB, store it to the local + filesystem as a drive image named outimage.hda, and generate the outim‐ + age.hda.properties file with the drive's INQUIRY information: + scsidump -t 3 -f ./outimage.hda -s 65536 -p + + Restore Mode: [SCSI2Pi host] ---> [SCSI Drive] Launch scsidump to re‐ + store/upload a drive image from the local file system to SCSI ID 0 with + block size 1MiB: + scsidump -r -t 0 -f ./outimage.hda -s 1048576 + +SEE ALSO + scsictl(1), s2p(1), scsiexec(1) + + Full documentation is available at: + + scsidump(1) diff --git a/cpp/doc/s2pexec.1 b/cpp/doc/s2pexec.1 new file mode 100644 index 00000000..200de0f8 --- /dev/null +++ b/cpp/doc/s2pexec.1 @@ -0,0 +1,57 @@ +.TH scsiexec 1 +.SH NAME +scsiexec \- SCSI command execution tool for SCSI2Pi +.SH SYNOPSIS +.B scsiexec +\fB\-t\fR \fIID[:LUN]\fR +[\fB\-i\fR \fIBID\fR] +\fB\-f\fR \fIFILENAME\fR +[\fB\-b\fR] +[\fB\-F\fR] +[\fB\-B\fR] +[\fB\-T\fR] +[\fB\-t\tR] ID[:LUN] +[\fB\-L\fR \fILOG_LEVEL\fR] +[\fB\-X\fR] +.SH DESCRIPTION +.B scsiexec +wraps protobuf input data into a custom SCSI2Pi SCSI command, has s2p execute it and display the results. The input data must be a legal command for the SCSI2Pi protobuf interface. See the file s2p_interface.proto for details. + +.SH NOTES + +.B scsiexec +helps with advanced testing. It requires two connected SCSI2Pi boards. The board scsiexec is running on must be a FULLSPEC board because scsiexec requires SCSI initiator mode support. + +.SH OPTIONS +.TP +.BR \-t\fI " "\fIID[:LUN] +SCSI ID and optional LUN of the second SCSI2Pi board. The remote SCSI device will be functioning as the "Target" device. +.TP +.BR \-i\fI " "\fIBID +SCSI ID of the SCSI2Pi device. If not specified, the SCSI2Pi device will use ID 7. The SCSI2Pi host will be functioning as the "Initiator" device. +.TP +.BR \-f\fI " "\fIFILE +The protobuf data input file, by default in JSON format. +.TP +.BR \-b\fI +Signals that the input file is in protobuf binary format. +.TP +.BR \-F\fI +Signals that the input file is in protobuf text format. +.TP +.BR \-B\fI +Generate a protobuf binary format file. +.TP +.BR \-T\fI +Generate a protobuf text format file. +.TP +.BR \-L\fI " " \fILOG_LEVEL +Set the log level (trace, debug, info, warning, error, off). The default log level is 'info'. +.TP +.BR \-X\fI +Shut down s2p with a SCSI command. + +.SH SEE ALSO +scsictl(1), s2p(1), scsidump(1), scsiexec(1) + +Full documentation is available at: diff --git a/cpp/doc/s2pexec_man_page.txt b/cpp/doc/s2pexec_man_page.txt new file mode 100644 index 00000000..825ae15d --- /dev/null +++ b/cpp/doc/s2pexec_man_page.txt @@ -0,0 +1,104 @@ +scsiexec(1) General Commands Manual scsiexec(1) + +NAME + scsiexec - SCSI command execution tool for SCSI2Pi + +SYNOPSIS + scsiexec -t ID[:LUN] [-i BID] -f FILENAME [-b] [-F] [-B] [-T] [-tR] + ID[:LUN] [-L LOG_LEVEL] [-X] + +DESCRIPTION + scsiexec wraps protobuf input data into a custom SCSI2Pi SCSI command, + has s2p execute it and display the results. The input data must be a + legal command for the SCSI2Pi protobuf interface. See the file s2p_in‐ + terface.proto for details. + +NOTES + scsiexec helps with advanced testing. It requires two connected SCSI2Pi + boards. The board scsiexec is running on must be a FULLSPEC board be‐ + cause scsiexec requires SCSI initiator mode support. + +OPTIONS + -t ID[:LUN] + SCSI ID and optional LUN of the second SCSI2Pi board. The remote + SCSI device will be functioning as the "Target" device. + + -i BID SCSI ID of the SCSI2Pi device. If not specified, the SCSI2Pi de‐ + vice will use ID 7. The SCSI2Pi host will be functioning as the + "Initiator" device. + + -f FILE + The protobuf data input file, by default in JSON format. + + -b Signals that the input file is in protobuf binary format. + + -F Signals that the input file is in protobuf text format. + + -B Generate a protobuf binary format file. + + -T Generate a protobuf text format file. + + -L LOG_LEVEL + Set the log level (trace, debug, info, warning, error, off). The + default log level is 'info'. + + -X Shut down s2p with a SCSI command. + +SEE ALSO + scsictl(1), s2p(1), scsidump(1), scsiexec(1) + + Full documentation is available at: + + scsiexec(1) +scsiexec(1) General Commands Manual scsiexec(1) + +NAME + scsiexec - SCSI command execution tool for SCSI2Pi + +SYNOPSIS + scsiexec -t ID[:LUN] [-i BID] -f FILENAME [-b] [-F] [-B] [-T] [-tR] + ID[:LUN] [-L LOG_LEVEL] [-X] + +DESCRIPTION + scsiexec wraps protobuf input data into a custom SCSI2Pi SCSI command, + has s2p execute it and display the results. The input data must be a + legal command for the SCSI2Pi protobuf interface. See the file s2p_in‐ + terface.proto for details. + +NOTES + scsiexec helps with advanced testing. It requires two connected SCSI2Pi + boards. The board scsiexec is running on must be a FULLSPEC board be‐ + cause scsiexec requires SCSI initiator mode support. + +OPTIONS + -t ID[:LUN] + SCSI ID and optional LUN of the second SCSI2Pi board. The remote + SCSI device will be functioning as the "Target" device. + + -i BID SCSI ID of the SCSI2Pi device. If not specified, the SCSI2Pi de‐ + vice will use ID 7. The SCSI2Pi host will be functioning as the + "Initiator" device. + + -f FILE + The protobuf data input file, by default in JSON format. + + -b Signals that the input file is in protobuf binary format. + + -F Signals that the input file is in protobuf text format. + + -B Generate a protobuf binary format file. + + -T Generate a protobuf text format file. + + -L LOG_LEVEL + Set the log level (trace, debug, info, warning, error, off). The + default log level is 'info'. + + -X Shut down s2p with a SCSI command. + +SEE ALSO + scsictl(1), s2p(1), scsidump(1), scsiexec(1) + + Full documentation is available at: + + scsiexec(1) diff --git a/cpp/s2p/command_context.cpp b/cpp/s2p/command_context.cpp new file mode 100644 index 00000000..46d71e8a --- /dev/null +++ b/cpp/s2p/command_context.cpp @@ -0,0 +1,104 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/shared_exceptions.h" +#include "shared/protobuf_util.h" +#include "command_context.h" +#include +#include + +using namespace std; +using namespace command_interface; +using namespace protobuf_util; + +bool CommandContext::ReadCommand() +{ + // Read magic string + array magic; + if (const size_t bytes_read = ReadBytes(fd, magic); bytes_read) { + if (bytes_read != magic.size() || memcmp(magic.data(), "RASCSI", magic.size())) { + throw io_exception("Invalid magic"); + } + + // Fetch the command + DeserializeMessage(fd, command); + + return true; + } + + return false; +} + +void CommandContext::WriteResult(const PbResult& result) const +{ + // The descriptor is -1 when devices are not attached via the remote interface but by s2p + if (fd != -1) { + SerializeMessage(fd, result); + } +} + +bool CommandContext::WriteSuccessResult(PbResult& result) const +{ + result.set_status(true); + WriteResult(result); + return true; +} + +bool CommandContext::ReturnLocalizedError(LocalizationKey key, const string& arg1, const string& arg2, + const string& arg3) const +{ + return ReturnLocalizedError(key, NO_ERROR_CODE, arg1, arg2, arg3); +} + +bool CommandContext::ReturnLocalizedError(LocalizationKey key, PbErrorCode error_code, const string& arg1, + const string& arg2, const string& arg3) const +{ + // For the logfile always use English + // Do not log unknown operations as an error for backward/foward compatibility with old/new clients + if (error_code == PbErrorCode::UNKNOWN_OPERATION) { + spdlog::trace(localizer.Localize(key, "en", arg1, arg2, arg3)); + } + else { + spdlog::error(localizer.Localize(key, "en", arg1, arg2, arg3)); + } + + return ReturnStatus(false, localizer.Localize(key, locale, arg1, arg2, arg3), error_code, false); +} + +bool CommandContext::ReturnStatus(bool status, const string& msg, PbErrorCode error_code, bool log) const +{ + // Do not log twice if logging has already been done in the localized error handling above + if (log && !status && !msg.empty()) { + spdlog::error(msg); + } + + if (fd == -1) { + if (!msg.empty()) { + cerr << "Error: " << msg << endl; + } + } + else { + PbResult result; + result.set_status(status); + result.set_error_code(error_code); + result.set_msg(msg); + WriteResult(result); + } + + return status; +} + +bool CommandContext::ReturnSuccessStatus() const +{ + return ReturnStatus(true, "", PbErrorCode::NO_ERROR_CODE, true); +} + +bool CommandContext::ReturnErrorStatus(const string& msg) const +{ + return ReturnStatus(false, msg, PbErrorCode::NO_ERROR_CODE, true); +} diff --git a/cpp/s2p/command_context.h b/cpp/s2p/command_context.h new file mode 100644 index 00000000..012609b7 --- /dev/null +++ b/cpp/s2p/command_context.h @@ -0,0 +1,52 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "localizer.h" +#include "generated/command_interface.pb.h" +#include + +using namespace std; +using namespace command_interface; + +class CommandContext +{ + +public: + + CommandContext(const PbCommand& cmd, string_view f, string_view l) : command(cmd), default_folder(f), locale(l) {} + explicit CommandContext(int f) : fd(f) {} + ~CommandContext() = default; + + string GetDefaultFolder() const { return default_folder; } + void SetDefaultFolder(string_view f) { default_folder = f; } + bool ReadCommand(); + void WriteResult(const PbResult&) const; + bool WriteSuccessResult(PbResult&) const; + const PbCommand& GetCommand() const { return command; } + + bool ReturnLocalizedError(LocalizationKey, const string& = "", const string& = "", const string& = "") const; + bool ReturnLocalizedError(LocalizationKey, PbErrorCode, const string& = "", const string& = "", const string& = "") const; + bool ReturnSuccessStatus() const; + bool ReturnErrorStatus(const string&) const; + +private: + + bool ReturnStatus(bool, const string&, PbErrorCode, bool) const; + + const Localizer localizer; + + PbCommand command; + + string default_folder; + + string locale; + + int fd = -1; +}; diff --git a/cpp/s2p/command_dispatcher.cpp b/cpp/s2p/command_dispatcher.cpp new file mode 100644 index 00000000..c9344c20 --- /dev/null +++ b/cpp/s2p/command_dispatcher.cpp @@ -0,0 +1,281 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "controllers/controller_factory.h" +#include "s2p/command_dispatcher.h" +#include "shared/s2p_util.h" +#include "shared/protobuf_util.h" +#include "shared/shared_exceptions.h" +#include + +using namespace std; +using namespace spdlog; +using namespace command_interface; +using namespace s2p_util; +using namespace protobuf_util; + +bool CommandDispatcher::DispatchCommand(const CommandContext& context, PbResult& result, const string& identifier) +{ + const PbCommand& command = context.GetCommand(); + const PbOperation operation = command.operation(); + + if (!PbOperation_IsValid(operation)) { + spdlog::trace("Ignored unknown command with operation opcode " + to_string(operation)); + + return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION, UNKNOWN_OPERATION, to_string(operation)); + } + + spdlog::trace(identifier + "Executing " + PbOperation_Name(operation) + " command"); + + switch (operation) { + case LOG_LEVEL: + if (const string log_level = GetParam(command, "level"); !SetLogLevel(log_level)) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_LOG_LEVEL, log_level); + } + else { + return context.ReturnSuccessStatus(); + } + + case DEFAULT_FOLDER: + if (const string error = s2p_image.SetDefaultFolder(GetParam(command, "folder")); !error.empty()) { + context.WriteResult(result); + return false; + } + else { + return context.WriteSuccessResult(result); + } + + case DEVICES_INFO: + response.GetDevicesInfo(executor.Get_allDevices(), result, command, s2p_image.GetDefaultFolder()); + return context.WriteSuccessResult(result); + + case DEVICE_TYPES_INFO: + response.GetDeviceTypesInfo(*result.mutable_device_types_info()); + return context.WriteSuccessResult(result); + + case SERVER_INFO: + response.GetServerInfo(*result.mutable_server_info(), command, executor.Get_allDevices(), + executor.GetReservedIds(), s2p_image.GetDefaultFolder(), s2p_image.GetDepth()); + return context.WriteSuccessResult(result); + + case VERSION_INFO: + response.GetVersionInfo(*result.mutable_version_info()); + return context.WriteSuccessResult(result); + + case LOG_LEVEL_INFO: + response.GetLogLevelInfo(*result.mutable_log_level_info()); + return context.WriteSuccessResult(result); + + case DEFAULT_IMAGE_FILES_INFO: + response.GetImageFilesInfo(*result.mutable_image_files_info(), s2p_image.GetDefaultFolder(), + GetParam(command, "folder_pattern"), GetParam(command, "file_pattern"), s2p_image.GetDepth()); + return context.WriteSuccessResult(result); + + case IMAGE_FILE_INFO: + if (string filename = GetParam(command, "file"); filename.empty()) { + context.ReturnLocalizedError(LocalizationKey::ERROR_MISSING_FILENAME); + } + else { + auto image_file = make_unique(); + const bool status = response.GetImageFile(*image_file.get(), s2p_image.GetDefaultFolder(), + filename); + if (status) { + result.set_allocated_image_file_info(image_file.get()); + result.set_status(true); + context.WriteResult(result); + } + else { + context.ReturnLocalizedError(LocalizationKey::ERROR_IMAGE_FILE_INFO); + } + } + break; + + case NETWORK_INTERFACES_INFO: + response.GetNetworkInterfacesInfo(*result.mutable_network_interfaces_info()); + return context.WriteSuccessResult(result); + + case MAPPING_INFO: + response.GetMappingInfo(*result.mutable_mapping_info()); + return context.WriteSuccessResult(result); + + case STATISTICS_INFO: + response.GetStatisticsInfo(*result.mutable_statistics_info(), executor.Get_allDevices()); + return context.WriteSuccessResult(result); + + case OPERATION_INFO: + response.GetOperationInfo(*result.mutable_operation_info(), s2p_image.GetDepth()); + return context.WriteSuccessResult(result); + + case RESERVED_IDS_INFO: + response.GetReservedIds(*result.mutable_reserved_ids_info(), executor.GetReservedIds()); + return context.WriteSuccessResult(result); + + case SHUT_DOWN: + return ShutDown(context, GetParam(command, "mode")); + + case NO_OPERATION: + return context.ReturnSuccessStatus(); + + case CREATE_IMAGE: + return s2p_image.CreateImage(context); + + case DELETE_IMAGE: + return s2p_image.DeleteImage(context); + + case RENAME_IMAGE: + return s2p_image.RenameImage(context); + + case COPY_IMAGE: + return s2p_image.CopyImage(context); + + case PROTECT_IMAGE: + case UNPROTECT_IMAGE: + return s2p_image.SetImagePermissions(context); + + case RESERVE_IDS: + return executor.ProcessCmd(context); + + default: + // TODO Verify, especially for host services device + // The remaining commands may only be executed when the target is idle + if (!ExecuteWithLock(context)) { + return false; + } + + return HandleDeviceListChange(context, operation); + } + + return true; +} + +bool CommandDispatcher::ExecuteWithLock(const CommandContext& context) +{ + scoped_lock lock(executor.GetExecutionLocker()); + return executor.ProcessCmd(context); +} + +bool CommandDispatcher::HandleDeviceListChange(const CommandContext& context, PbOperation operation) const +{ + // ATTACH and DETACH return the resulting device list + if (operation == ATTACH || operation == DETACH) { + // A command with an empty device list is required here in order to return data for all devices + PbCommand command; + PbResult result; + response.GetDevicesInfo(executor.Get_allDevices(), result, command, s2p_image.GetDefaultFolder()); + context.WriteResult(result); + return result.status(); + } + + return true; +} + +// Shutdown on a remote interface command +bool CommandDispatcher::ShutDown(const CommandContext& context, const string& m) const +{ + if (m.empty()) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_SHUTDOWN_MODE_MISSING); + } + + AbstractController::shutdown_mode mode = AbstractController::shutdown_mode::NONE; + if (m == "rascsi") { + mode = AbstractController::shutdown_mode::STOP_S2P; + } + else if (m == "system") { + mode = AbstractController::shutdown_mode::STOP_PI; + } + else if (m == "reboot") { + mode = AbstractController::shutdown_mode::RESTART_PI; + } + else { + return context.ReturnLocalizedError(LocalizationKey::ERROR_SHUTDOWN_MODE_INVALID, m); + } + + // Shutdown modes other than rascsi require root permissions + if (mode != AbstractController::shutdown_mode::STOP_S2P && getuid()) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_SHUTDOWN_PERMISSION); + } + + // Report success now because after a shutdown nothing can be reported anymore + PbResult result; + context.WriteSuccessResult(result); + + return ShutDown(mode); +} + +// Shutdown on a SCSI command +bool CommandDispatcher::ShutDown(AbstractController::shutdown_mode mode) const +{ + switch(mode) { + case AbstractController::shutdown_mode::STOP_S2P: + spdlog::info("s2p shutdown requested"); + return true; + + case AbstractController::shutdown_mode::STOP_PI: + spdlog::info("Raspberry Pi shutdown requested"); + if (system("init 0") == -1) { + spdlog::error("Raspberry Pi shutdown failed"); + } + break; + + case AbstractController::shutdown_mode::RESTART_PI: + spdlog::info("Raspberry Pi restart requested"); + if (system("init 6") == -1) { + spdlog::error("Raspberry Pi restart failed"); + } + break; + + case AbstractController::shutdown_mode::NONE: + assert(false); + break; + } + + return false; +} + +bool CommandDispatcher::SetLogLevel(const string& log_level) +{ + int id = -1; + int lun = -1; + string level = log_level; + + if (const auto& components = Split(log_level, COMPONENT_SEPARATOR, 2); !components.empty()) { + level = components[0]; + + if (components.size() > 1) { + if (const string error = ProcessId(ControllerFactory::GetIdMax(), ControllerFactory::GetLunMax(), + components[1], id, lun); !error.empty()) { + spdlog::warn("Error setting log level: " + error); + return false; + } + } + } + + const level::level_enum l = level::from_str(level); + // Compensate for spdlog using 'off' for unknown levels + if (to_string_view(l) != level) { + spdlog::warn("Invalid log level '" + level + "'"); + return false; + } + + set_level(l); + DeviceLogger::SetLogIdAndLun(id, lun); + + if (id != -1) { + if (lun == -1) { + spdlog::info("Set log level for device " + to_string(id) + " to '" + level + "'"); + } + else { + spdlog::info("Set log level for device " + to_string(id) + ":" + to_string(lun) + " to '" + level + "'"); + } + } + else { + spdlog::info("Set log level to '" + level + "'"); + } + + return true; +} diff --git a/cpp/s2p/command_dispatcher.h b/cpp/s2p/command_dispatcher.h new file mode 100644 index 00000000..0ad4a558 --- /dev/null +++ b/cpp/s2p/command_dispatcher.h @@ -0,0 +1,45 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "s2p/command_context.h" +#include "s2p/s2p_executor.h" +#include "s2p/s2p_image.h" +#include "s2p/s2p_response.h" +#include "generated/command_interface.pb.h" + +using namespace std; + +class CommandDispatcher +{ + +public: + + CommandDispatcher(S2pImage& i, S2pResponse& r, S2pExecutor& e) + : s2p_image(i), response(r), executor(e) { } + ~CommandDispatcher() = default; + + bool DispatchCommand(const CommandContext&, PbResult&, const string&); + + bool ShutDown(AbstractController::shutdown_mode) const; + + static bool SetLogLevel(const string&); + +private: + + bool ExecuteWithLock(const CommandContext&); + bool HandleDeviceListChange(const CommandContext&, PbOperation) const; + bool ShutDown(const CommandContext&, const string&) const; + + S2pImage& s2p_image; + + S2pResponse& response; + + S2pExecutor& executor; +}; diff --git a/cpp/s2p/localizer.cpp b/cpp/s2p/localizer.cpp new file mode 100644 index 00000000..e9430120 --- /dev/null +++ b/cpp/s2p/localizer.cpp @@ -0,0 +1,287 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "localizer.h" +#include +#include + +using namespace std; + +Localizer::Localizer() +{ + Add(LocalizationKey::ERROR_AUTHENTICATION, "en", "Authentication failed"); + Add(LocalizationKey::ERROR_AUTHENTICATION, "de", "Authentifizierung fehlgeschlagen"); + Add(LocalizationKey::ERROR_AUTHENTICATION, "sv", "Autentiseringen misslyckades"); + Add(LocalizationKey::ERROR_AUTHENTICATION, "fr", "Authentification éronnée"); + Add(LocalizationKey::ERROR_AUTHENTICATION, "es", "Fallo de autentificación"); + Add(LocalizationKey::ERROR_AUTHENTICATION, "zh", "认证失败"); + + Add(LocalizationKey::ERROR_OPERATION, "en", "Unknown operation: %1"); + Add(LocalizationKey::ERROR_OPERATION, "de", "Unbekannte Operation: %1"); + Add(LocalizationKey::ERROR_OPERATION, "sv", "Okänd operation: %1"); + Add(LocalizationKey::ERROR_OPERATION, "fr", "Opération inconnue: %1"); + Add(LocalizationKey::ERROR_OPERATION, "es", "Operación desconocida: %1"); + Add(LocalizationKey::ERROR_OPERATION, "zh", "未知操作: %1"); + + Add(LocalizationKey::ERROR_LOG_LEVEL, "en", "Invalid log level '%1'"); + Add(LocalizationKey::ERROR_LOG_LEVEL, "de", "Ungültiger Log-Level '%1'"); + Add(LocalizationKey::ERROR_LOG_LEVEL, "sv", "Ogiltig loggnivå '%1'"); + Add(LocalizationKey::ERROR_LOG_LEVEL, "fr", "Niveau de journalisation invalide '%1'"); + Add(LocalizationKey::ERROR_LOG_LEVEL, "es", "Nivel de registro '%1' no válido"); + Add(LocalizationKey::ERROR_LOG_LEVEL, "zh", "无效的日志级别 '%1'"); + + Add(LocalizationKey::ERROR_MISSING_DEVICE_ID, "en", "Missing device ID"); + Add(LocalizationKey::ERROR_MISSING_DEVICE_ID, "de", "Fehlende Geräte-ID"); + Add(LocalizationKey::ERROR_MISSING_DEVICE_ID, "sv", "Enhetens id saknas"); + Add(LocalizationKey::ERROR_MISSING_DEVICE_ID, "fr", "ID de périphérique manquante"); + Add(LocalizationKey::ERROR_MISSING_DEVICE_ID, "es", "Falta el ID del dispositivo"); + Add(LocalizationKey::ERROR_MISSING_DEVICE_ID, "zh", "缺少设备 ID"); + + Add(LocalizationKey::ERROR_MISSING_FILENAME, "en", "Missing filename"); + Add(LocalizationKey::ERROR_MISSING_FILENAME, "de", "Fehlender Dateiname"); + Add(LocalizationKey::ERROR_MISSING_FILENAME, "sv", "Filnamn saknas"); + Add(LocalizationKey::ERROR_MISSING_FILENAME, "fr", "Nom de fichier manquant"); + Add(LocalizationKey::ERROR_MISSING_FILENAME, "es", "Falta el nombre del archivo"); + Add(LocalizationKey::ERROR_MISSING_FILENAME, "zh", "缺少文件名"); + + Add(LocalizationKey::ERROR_DEVICE_MISSING_FILENAME, "en", "Device type %1 requires a filename"); + Add(LocalizationKey::ERROR_DEVICE_MISSING_FILENAME, "de", "Gerätetyp %1 erfordert einen Dateinamen"); + Add(LocalizationKey::ERROR_DEVICE_MISSING_FILENAME, "sv", "Enhetstypen %1 kräver ett filnamn"); + Add(LocalizationKey::ERROR_DEVICE_MISSING_FILENAME, "fr", "Périphérique de type %1 à besoin d'un nom de fichier"); + Add(LocalizationKey::ERROR_DEVICE_MISSING_FILENAME, "es", "El tipo de dispositivo %1 requiere un nombre de archivo"); + Add(LocalizationKey::ERROR_DEVICE_MISSING_FILENAME, "zh", "设备类型 %1 需要一个文件名"); + + Add(LocalizationKey::ERROR_IMAGE_IN_USE, "en", "Image file '%1' is already being used by device %2"); + Add(LocalizationKey::ERROR_IMAGE_IN_USE, "de", "Image-Datei '%1' wird bereits von Gerät %2 benutzt"); + Add(LocalizationKey::ERROR_IMAGE_IN_USE, "sv", "Skivbildsfilen '%1' används redan av nhet %2"); + Add(LocalizationKey::ERROR_IMAGE_IN_USE, "fr", "Le fichier d'image '%1' est déjà utilisé par périphérique %2"); + Add(LocalizationKey::ERROR_IMAGE_IN_USE, "es", "El archivo de imagen '%1' ya está siendo utilizado por dispositivo %2"); + Add(LocalizationKey::ERROR_IMAGE_IN_USE, "zh", "图像文件%1已被 ID %2"); + + Add(LocalizationKey::ERROR_IMAGE_FILE_INFO, "en", "Can't create image file info for '%1'"); + Add(LocalizationKey::ERROR_IMAGE_FILE_INFO, "de", "Image-Datei-Information für '%1' kann nicht erzeugt werden"); + Add(LocalizationKey::ERROR_IMAGE_FILE_INFO, "sv", "Kunde ej skapa skivbildsfilsinfo för '%1'"); + Add(LocalizationKey::ERROR_IMAGE_FILE_INFO, "fr", "Ne peux pas créer les informations du fichier image '%1'"); + Add(LocalizationKey::ERROR_IMAGE_FILE_INFO, "es", "No se puede crear información de archivo de imagen para '%1'"); + Add(LocalizationKey::ERROR_IMAGE_FILE_INFO, "zh", "无法为'%1'创建图像文件信息"); + + Add(LocalizationKey::ERROR_RESERVED_ID, "en", "Device ID %1 is reserved"); + Add(LocalizationKey::ERROR_RESERVED_ID, "de", "Geräte-ID %1 ist reserviert"); + Add(LocalizationKey::ERROR_RESERVED_ID, "sv", "Enhets-id %1 är reserverat"); + Add(LocalizationKey::ERROR_RESERVED_ID, "fr", "ID de périphérique %1 réservée"); + Add(LocalizationKey::ERROR_RESERVED_ID, "es", "El ID de dispositivo %1 está reservado"); + Add(LocalizationKey::ERROR_RESERVED_ID, "zh", "设备 ID %1 已保留"); + + Add(LocalizationKey::ERROR_NON_EXISTING_DEVICE, "en", "Command for non-existing ID %1"); + Add(LocalizationKey::ERROR_NON_EXISTING_DEVICE, "de", "Kommando für nicht existente ID %1"); + Add(LocalizationKey::ERROR_NON_EXISTING_DEVICE, "sv", "Kommando för id %1 som ej existerar"); + Add(LocalizationKey::ERROR_NON_EXISTING_DEVICE, "fr", "Commande pour ID %1 non-existant"); + Add(LocalizationKey::ERROR_NON_EXISTING_DEVICE, "es", "Comando para ID %1 no existente"); + Add(LocalizationKey::ERROR_NON_EXISTING_DEVICE, "zh", "不存在的 ID %1 的指令"); + + Add(LocalizationKey::ERROR_NON_EXISTING_UNIT, "en", "Command for non-existing ID %1, unit %2"); + Add(LocalizationKey::ERROR_NON_EXISTING_UNIT, "de", "Kommando für nicht existente ID %1, Einheit %2"); + Add(LocalizationKey::ERROR_NON_EXISTING_UNIT, "sv", "Kommando för id %1, enhetsnummer %2 som ej existerar"); + Add(LocalizationKey::ERROR_NON_EXISTING_UNIT, "fr", "Command pour ID %1, unité %2 non-existant"); + Add(LocalizationKey::ERROR_NON_EXISTING_UNIT, "es", "Comando para ID %1 inexistente, unidad %2"); + Add(LocalizationKey::ERROR_NON_EXISTING_UNIT, "zh", "不存在的 ID %1, 单元 %2 的指令"); + + Add(LocalizationKey::ERROR_UNKNOWN_DEVICE_TYPE, "en", "Unknown device type %1"); + Add(LocalizationKey::ERROR_UNKNOWN_DEVICE_TYPE, "de", "Unbekannter Gerätetyp %1"); + Add(LocalizationKey::ERROR_UNKNOWN_DEVICE_TYPE, "sv", "Obekant enhetstyp: %1"); + Add(LocalizationKey::ERROR_UNKNOWN_DEVICE_TYPE, "fr", "Type de périphérique inconnu %1"); + Add(LocalizationKey::ERROR_UNKNOWN_DEVICE_TYPE, "es", "Tipo de dispositivo desconocido %1"); + Add(LocalizationKey::ERROR_UNKNOWN_DEVICE_TYPE, "zh", "未知设备类型 %1"); + + Add(LocalizationKey::ERROR_MISSING_DEVICE_TYPE, "en", "Device type required for unknown extension of file '%1'"); + Add(LocalizationKey::ERROR_MISSING_DEVICE_TYPE, "de", "Gerätetyp erforderlich für unbekannte Extension der Datei '%1'"); + Add(LocalizationKey::ERROR_MISSING_DEVICE_TYPE, "sv", "Man måste ange enhetstyp för obekant filändelse '%1'"); + Add(LocalizationKey::ERROR_MISSING_DEVICE_TYPE, "fr", "Type de périphérique requis pour extension inconnue du fichier '%1'"); + Add(LocalizationKey::ERROR_MISSING_DEVICE_TYPE, "es", "Tipo de dispositivo requerido para la extensión desconocida del archivo '%1'"); + Add(LocalizationKey::ERROR_MISSING_DEVICE_TYPE, "zh", "文件'%1'的未知扩展名所需的设备类型"); + + Add(LocalizationKey::ERROR_DUPLICATE_ID, "en", "Duplicate ID %1, unit %2"); + Add(LocalizationKey::ERROR_DUPLICATE_ID, "de", "Doppelte ID %1, Einheit %2"); + Add(LocalizationKey::ERROR_DUPLICATE_ID, "sv", "Duplikat id %1, enhetsnummer %2"); + Add(LocalizationKey::ERROR_DUPLICATE_ID, "fr", "ID %1, unité %2 dupliquée"); + Add(LocalizationKey::ERROR_DUPLICATE_ID, "es", "ID duplicado %1, unidad %2"); + Add(LocalizationKey::ERROR_DUPLICATE_ID, "zh", "重复 ID %1,单元 %2"); + + Add(LocalizationKey::ERROR_DETACH, "en", "Couldn't detach device"); + Add(LocalizationKey::ERROR_DETACH, "de", "Geräte konnte nicht entfernt werden"); + Add(LocalizationKey::ERROR_DETACH, "sv", "Kunde ej koppla ifrån enheten"); + Add(LocalizationKey::ERROR_DETACH, "fr", "Impossible de détacher le périphérique"); + Add(LocalizationKey::ERROR_DETACH, "es", "No se ha podido desconectar el dispositivo"); + Add(LocalizationKey::ERROR_DETACH, "zh", "无法卸载设备"); + + Add(LocalizationKey::ERROR_EJECT_REQUIRED, "en", "Existing medium must first be ejected"); + Add(LocalizationKey::ERROR_EJECT_REQUIRED, "de", "Das vorhandene Medium muss erst ausgeworfen werden"); + Add(LocalizationKey::ERROR_EJECT_REQUIRED, "sv", "Nuvarande skiva måste utmatas först"); + Add(LocalizationKey::ERROR_EJECT_REQUIRED, "fr", "Media déjà existant doit d'abord être éjecté"); + Add(LocalizationKey::ERROR_EJECT_REQUIRED, "es", "El medio existente debe ser expulsado primero"); + Add(LocalizationKey::ERROR_EJECT_REQUIRED, "zh", "必须先弹出现有介质"); + + Add(LocalizationKey::ERROR_DEVICE_NAME_UPDATE, "en", "Once set the device name cannot be changed anymore"); + Add(LocalizationKey::ERROR_DEVICE_NAME_UPDATE, "de", "Ein bereits gesetzter Gerätename kann nicht mehr geändert werden"); + Add(LocalizationKey::ERROR_DEVICE_NAME_UPDATE, "sv", "Enhetsnamn kan ej ändras efter att ha fastställts en gång"); + Add(LocalizationKey::ERROR_DEVICE_NAME_UPDATE, "fr", "Une fois défini, le nom de périphérique ne peut plus être changé"); + Add(LocalizationKey::ERROR_DEVICE_NAME_UPDATE, "es", "Una vez establecido el nombre del dispositivo ya no se puede cambiar"); + Add(LocalizationKey::ERROR_DEVICE_NAME_UPDATE, "zh", "设备名称设置后不能再更改"); + + Add(LocalizationKey::ERROR_SHUTDOWN_MODE_MISSING, "en", "Missing shutdown mode"); + Add(LocalizationKey::ERROR_SHUTDOWN_MODE_MISSING, "de", "Fehlender Shutdown-Modus"); + Add(LocalizationKey::ERROR_SHUTDOWN_MODE_MISSING, "sv", "Avstängningsläge saknas"); + Add(LocalizationKey::ERROR_SHUTDOWN_MODE_MISSING, "fr", "Mode d'extinction manquant"); + Add(LocalizationKey::ERROR_SHUTDOWN_MODE_MISSING, "es", "Falta el modo de apagado"); + Add(LocalizationKey::ERROR_SHUTDOWN_MODE_MISSING, "zh", "缺少关机模式"); + + Add(LocalizationKey::ERROR_SHUTDOWN_MODE_INVALID, "en", "Invalid shutdown mode '%1'"); + Add(LocalizationKey::ERROR_SHUTDOWN_MODE_INVALID, "de", "Ungültiger Shutdown-Modus '%1'"); + Add(LocalizationKey::ERROR_SHUTDOWN_MODE_INVALID, "sv", "Ogiltigt avstängsningsläge: '%1'"); + Add(LocalizationKey::ERROR_SHUTDOWN_MODE_INVALID, "fr", "Mode d'extinction invalide '%1'"); + Add(LocalizationKey::ERROR_SHUTDOWN_MODE_INVALID, "es", "Modo de apagado inválido '%1'"); + Add(LocalizationKey::ERROR_SHUTDOWN_MODE_INVALID, "zh", "无效的关机模式 '%1'"); + + Add(LocalizationKey::ERROR_SHUTDOWN_PERMISSION, "en", "Missing root permission for shutdown or reboot"); + Add(LocalizationKey::ERROR_SHUTDOWN_PERMISSION, "de", "Fehlende Root-Berechtigung für Shutdown oder Neustart"); + Add(LocalizationKey::ERROR_SHUTDOWN_PERMISSION, "sv", "Saknar root-rättigheter för att kunna stänga av eller starta om systemet"); + Add(LocalizationKey::ERROR_SHUTDOWN_PERMISSION, "fr", "Permissions root manquantes pour extinction ou redémarrage"); + Add(LocalizationKey::ERROR_SHUTDOWN_PERMISSION, "es", "Falta el permiso de root para el apagado o el reinicio"); + Add(LocalizationKey::ERROR_SHUTDOWN_PERMISSION, "zh", "缺少关机或重启的 root 权限"); + + Add(LocalizationKey::ERROR_FILE_OPEN, "en", "Invalid or non-existing file '%1'"); + Add(LocalizationKey::ERROR_FILE_OPEN, "de", "Ungültige oder fehlende Datei '%1'"); + Add(LocalizationKey::ERROR_FILE_OPEN, "sv", "Ogiltig eller saknad fil '%1'"); + Add(LocalizationKey::ERROR_FILE_OPEN, "fr", "Fichier invalide ou non-existant '%1'"); + Add(LocalizationKey::ERROR_FILE_OPEN, "es", "Archivo inválido o inexistente '%1'"); + Add(LocalizationKey::ERROR_FILE_OPEN, "zh", "文件'%1'无效或不存在"); + + Add(LocalizationKey::ERROR_BLOCK_SIZE, "en", "Invalid block size %1 bytes"); + Add(LocalizationKey::ERROR_BLOCK_SIZE, "de", "Ungültige Blockgröße %1 Bytes"); + Add(LocalizationKey::ERROR_BLOCK_SIZE, "sv", "Ogiltig blockstorlek: %1 byte"); + Add(LocalizationKey::ERROR_BLOCK_SIZE, "fr", "Taille de bloc invalide %1 octets"); + Add(LocalizationKey::ERROR_BLOCK_SIZE, "es", "Tamaño de bloque inválido %1 bytes"); + Add(LocalizationKey::ERROR_BLOCK_SIZE, "zh", "无效的块大小 %1 字节"); + + Add(LocalizationKey::ERROR_BLOCK_SIZE_NOT_CONFIGURABLE, "en", "Block size for device type %1 is not configurable"); + Add(LocalizationKey::ERROR_BLOCK_SIZE_NOT_CONFIGURABLE, "de", "Blockgröße für Gerätetyp %1 ist nicht konfigurierbar"); + Add(LocalizationKey::ERROR_BLOCK_SIZE_NOT_CONFIGURABLE, "sv", "Enhetstypen %1 kan inte använda andra blockstorlekar"); + Add(LocalizationKey::ERROR_BLOCK_SIZE_NOT_CONFIGURABLE, "fr", "Taille de block pour le type de périphérique %1 non configurable"); + Add(LocalizationKey::ERROR_BLOCK_SIZE_NOT_CONFIGURABLE, "es", "El tamaño del bloque para el tipo de dispositivo %1 no es configurable"); + Add(LocalizationKey::ERROR_BLOCK_SIZE_NOT_CONFIGURABLE, "zh", "设备类型 %1 的块大小不可配置"); + + Add(LocalizationKey::ERROR_SCSI_CONTROLLER, "en", "Couldn't create SCSI controller"); + Add(LocalizationKey::ERROR_SCSI_CONTROLLER, "de", "SCSI-Controller konnte nicht erzeugt werden"); + Add(LocalizationKey::ERROR_SCSI_CONTROLLER, "sv", "Kunde ej skapa SCSI-gränssnitt"); + Add(LocalizationKey::ERROR_SCSI_CONTROLLER, "fr", "Impossible de créer le contrôleur SCSI"); + Add(LocalizationKey::ERROR_SCSI_CONTROLLER, "es", "No se ha podido crear el controlador SCSI"); + Add(LocalizationKey::ERROR_SCSI_CONTROLLER, "zh", "无法创建 SCSI 控制器"); + + Add(LocalizationKey::ERROR_INVALID_ID, "en", "Invalid device ID %1 (0-%2)"); + Add(LocalizationKey::ERROR_INVALID_ID, "de", "Ungültige Geräte-ID %1 (0-%2)"); + Add(LocalizationKey::ERROR_INVALID_ID, "sv", "Ogiltigt enhets-id %1 (0-%2)"); + Add(LocalizationKey::ERROR_INVALID_ID, "fr", "ID de périphérique invalide %1 (0-%2)"); + Add(LocalizationKey::ERROR_INVALID_ID, "es", "ID de dispositivo inválido %1 (0-%2)"); + Add(LocalizationKey::ERROR_INVALID_ID, "zh", "无效的设备 ID %1 (0-%2)"); + + Add(LocalizationKey::ERROR_INVALID_LUN, "en", "Invalid LUN %1 (0-%2)"); + Add(LocalizationKey::ERROR_INVALID_LUN, "de", "Ungültige LUN %1 (0-%2)"); + Add(LocalizationKey::ERROR_INVALID_LUN, "sv", "Ogiltigt enhetsnummer %1 (0-%2)"); + Add(LocalizationKey::ERROR_INVALID_LUN, "fr", "LUN invalide %1 (0-%2)"); + Add(LocalizationKey::ERROR_INVALID_LUN, "es", "LUN invalido %1 (0-%2)"); + Add(LocalizationKey::ERROR_INVALID_LUN, "zh", "无效的 LUN %1 (0-%2)"); + + Add(LocalizationKey::ERROR_LUN0, "en", "LUN 0 cannot be detached as long as there is still another LUN"); + Add(LocalizationKey::ERROR_LUN0, "de", "LUN 0 kann nicht entfernt werden, solange noch eine andere LUN existiert"); + Add(LocalizationKey::ERROR_LUN0, "sv", "Enhetsnummer 0 kan ej bli frånkopplat så länge som andra enhetsnummer är anslutna"); + Add(LocalizationKey::ERROR_LUN0, "fr", "LUN 0 ne peux pas être détaché tant qu'il y'a un autre LUN"); + Add(LocalizationKey::ERROR_LUN0, "es", "El LUN 0 no se puede desconectar mientras haya otro LUN"); + Add(LocalizationKey::ERROR_LUN0, "zh", "LUN 0 无法卸载,因为当前仍有另一个 LUN。"); + + Add(LocalizationKey::ERROR_INITIALIZATION, "en", "Initialization of %1 failed"); + Add(LocalizationKey::ERROR_INITIALIZATION, "de", "Initialisierung von %1 fehlgeschlagen"); + Add(LocalizationKey::ERROR_INITIALIZATION, "sv", "Kunde ej initialisera %1 "); + Add(LocalizationKey::ERROR_INITIALIZATION, "fr", "Echec de l'initialisation de %1"); + Add(LocalizationKey::ERROR_INITIALIZATION, "es", "La inicialización del %1 falló"); + Add(LocalizationKey::ERROR_INITIALIZATION, "zh", "%1 的初始化失败"); + + Add(LocalizationKey::ERROR_OPERATION_DENIED_STOPPABLE, "en", "%1 operation denied, %2 isn't stoppable"); + Add(LocalizationKey::ERROR_OPERATION_DENIED_STOPPABLE, "de", "%1-Operation verweigert, %2 ist nicht stopbar"); + Add(LocalizationKey::ERROR_OPERATION_DENIED_STOPPABLE, "sv", "Operationen %1 nekades för att %2 inte kan stoppas"); + Add(LocalizationKey::ERROR_OPERATION_DENIED_STOPPABLE, "fr", "Opération %1 refusée, %2 ne peut être stoppé"); + Add(LocalizationKey::ERROR_OPERATION_DENIED_STOPPABLE, "es", "%1 operación denegada, %2 no se puede parar"); + Add(LocalizationKey::ERROR_OPERATION_DENIED_STOPPABLE, "zh", "%1 操作被拒绝,%2 不可停止"); + + Add(LocalizationKey::ERROR_OPERATION_DENIED_REMOVABLE, "en", "%1 operation denied, %2 isn't removable"); + Add(LocalizationKey::ERROR_OPERATION_DENIED_REMOVABLE, "de", "%1-Operation verweigert, %2 ist nicht wechselbar"); + Add(LocalizationKey::ERROR_OPERATION_DENIED_REMOVABLE, "sv", "Operationen %1 nekades för att %2 inte är uttagbar(t)"); + Add(LocalizationKey::ERROR_OPERATION_DENIED_REMOVABLE, "fr", "Opération %1 refusée, %2 n'est pas détachable"); + Add(LocalizationKey::ERROR_OPERATION_DENIED_REMOVABLE, "es", "%1 operación denegada, %2 no es removible"); + Add(LocalizationKey::ERROR_OPERATION_DENIED_REMOVABLE, "zh", "%1 操作被拒绝,%2 不可移除"); + + Add(LocalizationKey::ERROR_OPERATION_DENIED_PROTECTABLE, "en", "%1 operation denied, %2 isn't protectable"); + Add(LocalizationKey::ERROR_OPERATION_DENIED_PROTECTABLE, "de", "%1-Operation verweigert, %2 ist nicht schützbar"); + Add(LocalizationKey::ERROR_OPERATION_DENIED_PROTECTABLE, "sv", "Operationen %1 nekades för att %2 inte är skyddbar(t)"); + Add(LocalizationKey::ERROR_OPERATION_DENIED_PROTECTABLE, "fr", "Opération %1 refusée, %2 n'est pas protégeable"); + Add(LocalizationKey::ERROR_OPERATION_DENIED_PROTECTABLE, "es", "%1 operación denegada, %2 no es protegible"); + Add(LocalizationKey::ERROR_OPERATION_DENIED_PROTECTABLE, "zh", "%1 操作被拒绝,%2 不可保护"); + + Add(LocalizationKey::ERROR_OPERATION_DENIED_READY, "en", "%1 operation denied, %2 isn't ready"); + Add(LocalizationKey::ERROR_OPERATION_DENIED_READY, "de", "%1-Operation verweigert, %2 ist nicht bereit"); + Add(LocalizationKey::ERROR_OPERATION_DENIED_READY, "sv", "Operationen %1 nekades för att %2 inte är redo"); + Add(LocalizationKey::ERROR_OPERATION_DENIED_READY, "fr", "Opération %1 refusée, %2 n'est pas prêt"); + Add(LocalizationKey::ERROR_OPERATION_DENIED_READY, "es", "%1 operación denegada, %2 no está listo"); + Add(LocalizationKey::ERROR_OPERATION_DENIED_READY, "zh", "%1 操作被拒绝,%2 还没有准备好"); + + Add(LocalizationKey::ERROR_UNIQUE_DEVICE_TYPE, "en", "There can only be a single %1 device"); + Add(LocalizationKey::ERROR_UNIQUE_DEVICE_TYPE, "de", "Es kann nur ein einziges %1-Gerät geben"); +} + +void Localizer::Add(LocalizationKey key, const string& locale, string_view value) +{ + // Safeguards against empty messages, duplicate entries and unsupported locales + assert(locale.size()); + assert(value.size()); + assert(supported_languages.contains(locale)); + assert(localized_messages[locale][key].empty()); + localized_messages[locale][key] = value; +} + +string Localizer::Localize(LocalizationKey key, const string& locale, const string& arg1, const string& arg2, + const string &arg3) const +{ + string locale_lower; + ranges::transform(locale, back_inserter(locale_lower), ::tolower); + + auto it = localized_messages.find(locale_lower); + if (it == localized_messages.end()) { + // Try to fall back to country-indepedent locale (e.g. "en" instead of "en_US") + if (locale_lower.length() > 2) { + it = localized_messages.find(locale_lower.substr(0, 2)); + } + if (it == localized_messages.end()) { + it = localized_messages.find("en"); + } + } + + assert (it != localized_messages.end()); + + auto messages = it->second; + + const auto& m = messages.find(key); + if (m == messages.end()) { + return "Missing localization for enum value " + to_string(static_cast(key)); + } + + string message = m->second; + message = regex_replace(message, regex1, arg1); + message = regex_replace(message, regex2, arg2); + message = regex_replace(message, regex3, arg3); + + return message; +} diff --git a/cpp/s2p/localizer.h b/cpp/s2p/localizer.h new file mode 100644 index 00000000..e4a7023c --- /dev/null +++ b/cpp/s2p/localizer.h @@ -0,0 +1,77 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +// Message localization support. Currently only for messages with up to 3 string parameters. +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "shared/s2p_util.h" +#include +#include +#include +#include + +using namespace std; + +enum class LocalizationKey { + ERROR_AUTHENTICATION, + ERROR_OPERATION, + ERROR_LOG_LEVEL, + ERROR_MISSING_DEVICE_ID, + ERROR_MISSING_FILENAME, + ERROR_DEVICE_MISSING_FILENAME, + ERROR_IMAGE_IN_USE, + ERROR_IMAGE_FILE_INFO, + ERROR_RESERVED_ID, + ERROR_NON_EXISTING_DEVICE, + ERROR_NON_EXISTING_UNIT, + ERROR_UNKNOWN_DEVICE_TYPE, + ERROR_MISSING_DEVICE_TYPE, + ERROR_DUPLICATE_ID, + ERROR_DETACH, + ERROR_EJECT_REQUIRED, + ERROR_DEVICE_NAME_UPDATE, + ERROR_SHUTDOWN_MODE_MISSING, + ERROR_SHUTDOWN_MODE_INVALID, + ERROR_SHUTDOWN_PERMISSION, + ERROR_FILE_OPEN, + ERROR_BLOCK_SIZE, + ERROR_BLOCK_SIZE_NOT_CONFIGURABLE, + ERROR_SCSI_CONTROLLER, + ERROR_INVALID_ID, + ERROR_INVALID_LUN, + ERROR_LUN0, + ERROR_INITIALIZATION, + ERROR_OPERATION_DENIED_STOPPABLE, + ERROR_OPERATION_DENIED_REMOVABLE, + ERROR_OPERATION_DENIED_PROTECTABLE, + ERROR_OPERATION_DENIED_READY, + ERROR_UNIQUE_DEVICE_TYPE +}; + +class Localizer +{ +public: + + Localizer(); + ~Localizer() = default; + + string Localize(LocalizationKey, const string&, const string& = "", const string& = "", const string& = "") const; + +private: + + void Add(LocalizationKey, const string&, string_view); + unordered_map, s2p_util::StringHash, equal_to<>> localized_messages; + + // Supported locales, always lower case + unordered_set> supported_languages = { "en", "de", "sv", "fr", "es", "zh" }; + + const regex regex1 = regex("%1"); + const regex regex2 = regex("%2"); + const regex regex3 = regex("%3"); +}; diff --git a/cpp/s2p/s2p.cpp b/cpp/s2p/s2p.cpp new file mode 100644 index 00000000..cebc8e4b --- /dev/null +++ b/cpp/s2p/s2p.cpp @@ -0,0 +1,18 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "s2p/s2p_core.h" + +using namespace std; + +int main(int argc, char *argv[]) +{ + vector args(argv, argv + argc); + + return S2p().run(args); +} diff --git a/cpp/s2p/s2p_core.cpp b/cpp/s2p/s2p_core.cpp new file mode 100644 index 00000000..a7741e5a --- /dev/null +++ b/cpp/s2p/s2p_core.cpp @@ -0,0 +1,471 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2016-2020 GIMONS +// Copyright (C) 2020-2023 Contributors to the PiSCSI project +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/s2p_util.h" +#include "shared/protobuf_util.h" +#include "shared/shared_exceptions.h" +#include "shared/s2p_version.h" +#ifdef BUILD_SCHS +#include "devices/host_services.h" +#endif +#include "buses/gpiobus.h" +#include "s2p/s2p_core.h" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace spdlog; +using namespace command_interface; +using namespace s2p_util; +using namespace protobuf_util; +using namespace scsi_defs; + +void S2p::Banner(span args) const +{ + cout << s2p_util::Banner("(Target Emulation Service)") + << "\nUsage: " << args[0] << " [-id|hd[:LUN]] FILE] ...\n\n" + << " id|ID is a SCSI device ID (0-" << (ControllerFactory::GetIdMax() - 1) << ").\n" + << " hd|HD is a SASI device ID (0-" << (ControllerFactory::GetIdMax() - 1) << ").\n" + << " LUN is the optional logical unit, 0 is the default" + << " (SCSI: 0-" << (ControllerFactory::GetScsiLunMax() - 1) << ")" + << ", SASI: 0-" << (ControllerFactory::GetSasiLunMax() - 1) << ").\n" + << " Attaching a SASI drive (-hd instead of -id) selects SASI compatibility.\n" + << " FILE is either a disk image file, \"daynaport\", \"printer\" or \"services\".\n" + << " The image type is derived from the extension when no type is specified:\n" + << " hd1: SCSI HD image (Non-removable SCSI-1-CCS HD image)\n" + << " hds: SCSI HD image (Non-removable SCSI-2 HD image)\n" + << " hda: SCSI HD image (Apple compatible non-removable SCSI-2 HD image)\n" + << " hdr: SCSI HD image (Removable SCSI-2 HD image)\n" + << " mos: SCSI MO image (SCSI-2 MO image)\n" + << " iso: SCSI CD image (SCSI-2 ISO 9660 image)\n" + << " is1: SCSI CD image (SCSI-1-CCS ISO 9660 image)\n" + << " Run 'man s2p' for other options.\n" << flush; + + exit(EXIT_SUCCESS); +} + +bool S2p::InitBus(bool in_process) +{ + bus_factory = make_unique(); + + bus = bus_factory->CreateBus(BUS::mode_e::TARGET, in_process); + if (!bus) { + return false; + } + + controller_factory = make_shared(is_sasi); + + executor = make_unique(*bus, controller_factory); + + dispatcher = make_shared(s2p_image, response, *executor); + + return true; +} + +void S2p::CleanUp() +{ + if (service.IsRunning()) { + service.Stop(); + } + + executor->DetachAll(); + + // TODO Check why there are rare cases where bus is NULL on a remote interface shutdown + // even though it is never set to NULL anywhere + if (bus) { + bus->CleanUp(); + } +} + +void S2p::ReadAccessToken(const path& filename) +{ + if (error_code error; !is_regular_file(filename, error)) { + throw parser_exception("Access token file '" + filename.string() + "' must be a regular file"); + } + + if (struct stat st; stat(filename.c_str(), &st) || st.st_uid || st.st_gid) { + throw parser_exception("Access token file '" + filename.string() + "' must be owned by root"); + } + + if (const auto perms = filesystem::status(filename).permissions(); + (perms & perms::group_read) != perms::none || (perms & perms::others_read) != perms::none || + (perms & perms::group_write) != perms::none || (perms & perms::others_write) != perms::none) { + throw parser_exception("Access token file '" + filename.string() + "' must be readable by root only"); + } + + ifstream token_file(filename); + if (token_file.fail()) { + throw parser_exception("Can't open access token file '" + filename.string() + "'"); + } + + getline(token_file, access_token); + if (token_file.fail()) { + throw parser_exception("Can't read access token file '" + filename.string() + "'"); + } + + if (access_token.empty()) { + throw parser_exception("Access token file '" + filename.string() + "' must not be empty"); + } +} + +void S2p::LogDevices(string_view devices) const +{ + stringstream ss(devices.data()); + string line; + + while (getline(ss, line, '\n')) { + spdlog::info(line); + } +} + +void S2p::TerminationHandler(int) +{ + instance->CleanUp(); + + // Process will terminate automatically +} + +string S2p::ParseArguments(span args, PbCommand& command, int& port, string& reserved_ids) +{ + string log_level = "info"; + PbDeviceType type = UNDEFINED; + int block_size = 0; + string name; + string id_and_lun; + + string locale = GetLocale(); + + // Avoid duplicate messages while parsing + set_level(level::off); + + optind = 1; + opterr = 0; + int opt; + while ((opt = getopt(static_cast(args.size()), args.data(), "-Ii-Hhb:d:n:p:r:t:z:C:D:F:L:P:R:vV")) != -1) { + switch (opt) { + // The two option pairs below are kind of a compound option with two letters + case 'i': + case 'I': + continue; + + case 'h': + case 'H': + is_sasi = true; + continue; + + case 'd': + case 'D': + id_and_lun = optarg; + continue; + + case 'b': + if (!GetAsUnsignedInt(optarg, block_size)) { + throw parser_exception("Invalid block size " + string(optarg)); + } + continue; + + case 'z': + locale = optarg; + continue; + + case 'F': + if (const string error = s2p_image.SetDefaultFolder(optarg); !error.empty()) { + throw parser_exception(error); + } + continue; + + case 'L': + log_level = optarg; + continue; + + case 'R': + int depth; + if (!GetAsUnsignedInt(optarg, depth)) { + throw parser_exception("Invalid image file scan depth " + string(optarg)); + } + s2p_image.SetDepth(depth); + continue; + + case 'V': + Banner(args); + break; + + case 'n': + name = optarg; + continue; + + case 'p': + if (!GetAsUnsignedInt(optarg, port) || port <= 0 || port > 65535) { + throw parser_exception("Invalid port " + string(optarg) + ", port must be between 1 and 65535"); + } + continue; + + case 'P': + ReadAccessToken(optarg); + continue; + + case 'r': + reserved_ids = optarg; + continue; + + case 't': + type = ParseDeviceType(optarg); + continue; + + case 1: + // Encountered filename + break; + + default: + Banner(args); + break; + } + + if (optopt) { + Banner(args); + break; + } + + // Set up the device data + + auto device = command.add_devices(); + + if (!id_and_lun.empty()) { + if (const string error = SetIdAndLun(ControllerFactory::GetIdMax(), ControllerFactory::GetLunMax(), + *device, id_and_lun); !error.empty()) { + throw parser_exception(error); + } + } + + device->set_type(type); + device->set_block_size(block_size); + + ParseParameters(*device, optarg); + + SetProductData(*device, name); + + type = UNDEFINED; + block_size = 0; + name = ""; + id_and_lun = ""; + } + + if (!CommandDispatcher::SetLogLevel(log_level)) { + throw parser_exception("Invalid log level '" + log_level + "'"); + } + + return locale; +} + +PbDeviceType S2p::ParseDeviceType(const string& value) +{ + string t; + ranges::transform(value, back_inserter(t), ::toupper); + if (PbDeviceType type; PbDeviceType_Parse(t, &type)) { + return type; + } + + throw parser_exception("Illegal device type '" + value + "'"); +} + +bool S2p::ExecuteWithLock(const CommandContext& context) +{ + scoped_lock lock(executor->GetExecutionLocker()); + return executor->ProcessCmd(context); +} + +bool S2p::HandleDeviceListChange(const CommandContext& context, PbOperation operation) const +{ + // ATTACH and DETACH return the resulting device list + if (operation == ATTACH || operation == DETACH) { + // A command with an empty device list is required here in order to return data for all devices + PbCommand command; + PbResult result; + response.GetDevicesInfo(executor->Get_allDevices(), result, command, s2p_image.GetDefaultFolder()); + context.WriteResult(result); + return result.status(); + } + + return true; +} + +int S2p::run(span args, bool in_process) +{ + GOOGLE_PROTOBUF_VERIFY_VERSION; + + if (!args.size()) { + Banner(args); + } + + // The -v option shall result in no other action except displaying the version + if (ranges::find_if(args, [] (const char *arg) { return !strcasecmp(arg, "-v"); } ) != args.end()) { + cout << GetVersionString() << '\n'; + return EXIT_SUCCESS; + } + + PbCommand command; + string locale; + string reserved_ids; + int port = DEFAULT_PORT; + try { + locale = ParseArguments(args, command, port, reserved_ids); + } + catch(const parser_exception& e) { + cerr << "Error: " << e.what() << endl; + return EXIT_FAILURE; + } + + if (!InitBus(in_process)) { + cerr << "Error: Can't initialize bus" << endl; + + return EXIT_FAILURE; + } + + if (const string error = service.Init([this] (CommandContext& context) { + return ExecuteCommand(context); + }, port); !error.empty()) { + cerr << "Error: " << error << endl; + CleanUp(); + return EXIT_FAILURE; + } + + if (const string error = executor->SetReservedIds(reserved_ids); !error.empty()) { + cerr << "Error: " << error << endl; + CleanUp(); + return EXIT_FAILURE; + } + + if (command.devices_size()) { + // Attach all specified devices + command.set_operation(ATTACH); + + if (const CommandContext context(command, s2p_image.GetDefaultFolder(), locale); !executor->ProcessCmd(context)) { + cerr << "Error: Can't attach devices" << endl; + CleanUp(); + return EXIT_FAILURE; + } + +#ifdef BUILD_SCHS + // Ensure that all host services have a dispatcher + for (auto device : controller_factory->GetAllDevices()) { + if (auto host_services = dynamic_pointer_cast(device); host_services != nullptr) { + host_services->SetDispatcher(dispatcher); + } + } +#endif + } + + // Display and log the device list + PbServerInfo server_info; + response.GetDevices(executor->Get_allDevices(), server_info, s2p_image.GetDefaultFolder()); + const vector& devices = { server_info.devices_info().devices().begin(), server_info.devices_info().devices().end() }; + const string device_list = ListDevices(devices); + LogDevices(device_list); + cout << device_list << flush; + + if (!bus_factory->IsRaspberryPi()) { + cout << "Note: No board hardware support, only client interface calls are supported\n" << flush; + } + + SetUpEnvironment(); + + service.Start(); + + ProcessScsiCommands(); + + return EXIT_SUCCESS; +} + +void S2p::SetUpEnvironment() +{ + instance = this; + + // Signal handler to detach all devices on a KILL or TERM signal + struct sigaction termination_handler; + termination_handler.sa_handler = TerminationHandler; + sigemptyset(&termination_handler.sa_mask); + termination_handler.sa_flags = 0; + sigaction(SIGINT, &termination_handler, nullptr); + sigaction(SIGTERM, &termination_handler, nullptr); + signal(SIGPIPE, SIG_IGN); +} + +void S2p::ProcessScsiCommands() +{ + while (service.IsRunning()) { + // Only process the SCSI command if the bus is not busy and no other device responded + // TODO There may be something wrong with the SEL/BSY handling, see PhaseExecutor/Arbitration + if (WaitForSelection() && WaitForNotBusy()) { + scoped_lock lock(executor->GetExecutionLocker()); + + // Process command on the responsible controller based on the current initiator and target ID + if (const auto shutdown_mode = controller_factory->ProcessOnController(bus->GetDAT()); + shutdown_mode != AbstractController::shutdown_mode::NONE) { + // When the bus is free SCSI2Pi or the Pi may be shut down. + dispatcher->ShutDown(shutdown_mode); + } + } + } +} + +bool S2p::ExecuteCommand(CommandContext& context) +{ + if (!access_token.empty() && access_token != GetParam(context.GetCommand(), "token")) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_AUTHENTICATION, UNAUTHORIZED); + } + + context.SetDefaultFolder(s2p_image.GetDefaultFolder()); + PbResult result; + const bool status = dispatcher->DispatchCommand(context, result, ""); + if (status && context.GetCommand().operation() == PbOperation::SHUT_DOWN) { + CleanUp(); + return false; + } + + return status; +} + +bool S2p::WaitForNotBusy() const +{ + // Wait up to 3 s for BSY to be released, signalling the end of the ARBITRATION phase + if (bus->GetBSY()) { + const auto now = chrono::steady_clock::now(); + while ((chrono::duration_cast(chrono::steady_clock::now() - now).count()) < 3) { + bus->Acquire(); + if (!bus->GetBSY()) { + return true; + } + } + + return false; + } + + return true; +} + +bool S2p::WaitForSelection() +{ + if (!bus->WaitForSelectEvent()) { + // Stop on interrupt + if (errno == EINTR) { + service.Stop(); + } + + return false; + } + + return true; +} diff --git a/cpp/s2p/s2p_core.h b/cpp/s2p/s2p_core.h new file mode 100644 index 00000000..1ec198a0 --- /dev/null +++ b/cpp/s2p/s2p_core.h @@ -0,0 +1,78 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "buses/bus_factory.h" +#include "controllers/controller_factory.h" +#include "s2p/command_context.h" +#include "s2p/command_dispatcher.h" +#include "s2p/s2p_service.h" +#include "s2p/s2p_image.h" +#include "s2p/s2p_response.h" +#include "s2p/s2p_executor.h" +#include "generated/command_interface.pb.h" +#include +#include + +using namespace std; + +class S2p +{ + static const int DEFAULT_PORT = 6868; + +public: + + S2p() = default; + ~S2p() = default; + + int run(span, bool = false); + +private: + + void Banner(span) const; + bool InitBus(bool); + void CleanUp(); + void ReadAccessToken(const path&); + void LogDevices(string_view) const; + static void TerminationHandler(int); + string ParseArguments(span, PbCommand&, int&, string&); + void SetUpEnvironment(); + void ProcessScsiCommands(); + bool WaitForNotBusy() const; + bool WaitForSelection(); + + bool ExecuteCommand(CommandContext&); + bool ExecuteWithLock(const CommandContext&); + bool HandleDeviceListChange(const CommandContext&, PbOperation) const; + + static PbDeviceType ParseDeviceType(const string&); + + bool is_sasi = false; + + string access_token; + + S2pImage s2p_image; + + [[no_unique_address]] S2pResponse response; + + S2pService service; + + unique_ptr executor; + + shared_ptr dispatcher; + + shared_ptr controller_factory; + + unique_ptr bus_factory; + + unique_ptr bus; + + // Required for the termination handler + static inline S2p *instance; +}; diff --git a/cpp/s2p/s2p_executor.cpp b/cpp/s2p/s2p_executor.cpp new file mode 100644 index 00000000..5d6594c9 --- /dev/null +++ b/cpp/s2p/s2p_executor.cpp @@ -0,0 +1,632 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/s2p_util.h" +#include "shared/protobuf_util.h" +#include "shared/shared_exceptions.h" +#include "devices/disk.h" +#include "localizer.h" +#include "command_context.h" +#include "s2p_executor.h" +#include +#include + +using namespace spdlog; +using namespace protobuf_util; +using namespace s2p_util; + +bool S2pExecutor::ProcessDeviceCmd(const CommandContext& context, const PbDeviceDefinition& pb_device, bool dryRun) +{ + spdlog::info((dryRun ? "Validating: " : "Executing: ") + PrintCommand(context.GetCommand(), pb_device)); + + const int id = pb_device.id(); + const int lun = pb_device.unit(); + + if (!ValidateIdAndLun(context, id, lun)) { + return false; + } + + const PbOperation operation = context.GetCommand().operation(); + + // For all commands except ATTACH the device and LUN must exist + if (operation != ATTACH && !VerifyExistingIdAndLun(context, id, lun)) { + return false; + } + + auto device = controller_factory->GetDeviceForIdAndLun(id, lun); + + if (!ValidateOperationAgainstDevice(context, *device, operation)) { + return false; + } + + switch (operation) { + case START: + return Start(*device, dryRun); + + case STOP: + return Stop(*device, dryRun); + + case ATTACH: + return Attach(context, pb_device, dryRun); + + case DETACH: + return Detach(context, *device, dryRun); + + case INSERT: + return Insert(context, pb_device, device, dryRun); + + case EJECT: + return Eject(*device, dryRun); + + case PROTECT: + return Protect(*device, dryRun); + + case UNPROTECT: + return Unprotect(*device, dryRun); + break; + + case CHECK_AUTHENTICATION: + case NO_OPERATION: + // Do nothing, just log + spdlog::trace("Received " + PbOperation_Name(operation) + " command"); + break; + + default: + return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION, to_string(operation)); + } + + return true; +} + +bool S2pExecutor::ProcessCmd(const CommandContext& context) +{ + const PbCommand& command = context.GetCommand(); + + // Handle commands that are not device-specific + switch (command.operation()) { + case DETACH_ALL: + DetachAll(); + return context.ReturnSuccessStatus(); + + case RESERVE_IDS: { + if (const string error = SetReservedIds(GetParam(command, "ids")); !error.empty()) { + return context.ReturnErrorStatus(error); + } + + return context.ReturnSuccessStatus(); + } + + default: + // This is a device-specific command handled below + break; + } + + // Remember the list of reserved files during the dry run + const auto& reserved_files = StorageDevice::GetReservedFiles(); + const bool isDryRunError = ranges::find_if_not(command.devices(), [&] (const auto& device) + { return ProcessDeviceCmd(context, device, true); }) != command.devices().end(); + StorageDevice::SetReservedFiles(reserved_files); + + if (isDryRunError) { + return false; + } + + if (const string error = EnsureLun0(command); !error.empty()) { + return context.ReturnErrorStatus(error); + } + + if (ranges::find_if_not(command.devices(), [&] (const auto& device) + { return ProcessDeviceCmd(context, device, false); } ) != command.devices().end()) { + return false; + } + + return context.ReturnSuccessStatus(); +} + +bool S2pExecutor::Start(PrimaryDevice& device, bool dryRun) const +{ + if (!dryRun) { + spdlog::info("Start requested for " + device.GetIdentifier()); + + if (!device.Start()) { + spdlog::warn("Starting " + device.GetIdentifier() + " failed"); + } + } + + return true; +} + +bool S2pExecutor::Stop(PrimaryDevice& device, bool dryRun) const +{ + if (!dryRun) { + spdlog::info("Stop requested for " + device.GetIdentifier()); + + device.Stop(); + } + + return true; +} + +bool S2pExecutor::Eject(PrimaryDevice& device, bool dryRun) const +{ + if (!dryRun) { + spdlog::info("Eject requested for " + device.GetIdentifier()); + + if (!device.Eject(true)) { + spdlog::warn("Ejecting " + device.GetIdentifier() + " failed"); + } + } + + return true; +} + +bool S2pExecutor::Protect(PrimaryDevice& device, bool dryRun) const +{ + if (!dryRun) { + spdlog::info("Write protection requested for " + device.GetIdentifier()); + + device.SetProtected(true); + } + + return true; +} + +bool S2pExecutor::Unprotect(PrimaryDevice& device, bool dryRun) const +{ + if (!dryRun) { + spdlog::info("Write unprotection requested for " + device.GetIdentifier()); + + device.SetProtected(false); + } + + return true; +} + +bool S2pExecutor::Attach(const CommandContext& context, const PbDeviceDefinition& pb_device, bool dryRun) +{ + const int id = pb_device.id(); + const int lun = pb_device.unit(); + + if (lun >= ControllerFactory::GetScsiLunMax()) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_INVALID_LUN, to_string(lun), + to_string(ControllerFactory::GetScsiLunMax())); + } + + if (controller_factory->HasDeviceForIdAndLun(id, lun)) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_DUPLICATE_ID, to_string(id), to_string(lun)); + } + + if (reserved_ids.contains(id)) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_RESERVED_ID, to_string(id)); + } + + + const string filename = GetParam(pb_device, "file"); + + const PbDeviceType type = pb_device.type(); + auto device = CreateDevice(context, type, lun, filename); + if (!device) { + return false; + } + + // If no filename was provided the medium is considered not inserted + device->SetRemoved(device->SupportsFile() ? filename.empty() : false); + + if (!SetProductData(context, pb_device, *device)) { + return false; + } + + if (!SetSectorSize(context, device, pb_device.block_size())) { + return false; + } + +#ifdef BUILD_DISK + const auto storage_device = dynamic_pointer_cast(device); + if (device->SupportsFile()) { + // Only with removable media drives, CD and MO the medium (=file) may be inserted later + if (!device->IsRemovable() && filename.empty()) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_MISSING_FILENAME, PbDeviceType_Name(type)); + } + + if (!ValidateImageFile(context, *storage_device, filename)) { + return false; + } + } +#endif + + // Only non read-only devices support protect/unprotect + // This operation must not be executed before Open() because Open() overrides some settings. + if (device->IsProtectable() && !device->IsReadOnly()) { + device->SetProtected(pb_device.protected_()); + } + + // Stop the dry run here, before actually attaching + if (dryRun) { + return true; + } + + param_map params = { pb_device.params().begin(), pb_device.params().end() }; + if (!device->SupportsFile()) { + // Clients like scsictl might have sent both "file" and "interfaces" + params.erase("file"); + } + + if (!device->Init(params)) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_INITIALIZATION, device->GetIdentifier()); + } + + if (!controller_factory->AttachToController(bus, id, device)) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_SCSI_CONTROLLER); + } + +#ifdef BUILD_DISK + if (storage_device && !storage_device->IsRemoved()) { + storage_device->ReserveFile(); + } +#endif + + string msg = "Attached "; + if (device->IsReadOnly()) { + msg += "read-only "; + } + else if (device->IsProtectable() && device->IsProtected()) { + msg += "protected "; + } + msg += device->GetIdentifier(); + spdlog::info(msg); + + return true; +} + +bool S2pExecutor::Insert(const CommandContext& context, const PbDeviceDefinition& pb_device, + const shared_ptr& device, bool dryRun) const +{ + if (!device->SupportsFile()) { + return false; + } + + if (!device->IsRemoved()) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_EJECT_REQUIRED); + } + + if (!pb_device.vendor().empty() || !pb_device.product().empty() || !pb_device.revision().empty()) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_DEVICE_NAME_UPDATE); + } + + const string filename = GetParam(pb_device, "file"); + if (filename.empty()) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_MISSING_FILENAME); + } + + // Stop the dry run here, before modifying the device + if (dryRun) { + return true; + } + + spdlog::info("Insert " + string(pb_device.protected_() ? "protected " : "") + "file '" + filename + + "' requested into " + device->GetIdentifier()); + + if (!SetSectorSize(context, device, pb_device.block_size())) { + return false; + } + +#ifdef BUILD_DISK + auto storage_device = dynamic_pointer_cast(device); + if (!ValidateImageFile(context, *storage_device, filename)) { + return false; + } + + storage_device->SetProtected(pb_device.protected_()); + storage_device->ReserveFile(); + storage_device->SetMediumChanged(true); +#endif + + return true; +} + +bool S2pExecutor::Detach(const CommandContext& context, PrimaryDevice& device, bool dryRun) const +{ + auto controller = controller_factory->FindController(device.GetId()); + if (controller == nullptr) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_DETACH); + } + + // LUN 0 can only be detached if there is no other LUN anymore + if (!device.GetLun() && controller->GetLunCount() > 1) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_LUN0); + } + + if (!dryRun) { + // Remember the device identifier for the log message before the device data become invalid on removal + const string identifier = device.GetIdentifier(); + + if (!controller->RemoveDevice(device)) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_DETACH); + } + + // If no LUN is left also delete the controller + if (!controller->GetLunCount() && !controller_factory->DeleteController(*controller)) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_DETACH); + } + + spdlog::info("Detached " + identifier); + } + + return true; +} + +void S2pExecutor::DetachAll() const +{ + controller_factory->DeleteAllControllers(); + + spdlog::info("Detached all devices"); +} + + +string S2pExecutor::SetReservedIds(string_view ids) +{ + set ids_to_reserve; + stringstream ss(ids.data()); + string id; + while (getline(ss, id, ',')) { + int res_id; + if (!GetAsUnsignedInt(id, res_id) || res_id > 7) { + return "Invalid ID " + id; + } + + if (controller_factory->HasController(res_id)) { + return "ID " + id + " is currently in use"; + } + + ids_to_reserve.insert(res_id); + } + + reserved_ids = { ids_to_reserve.begin(), ids_to_reserve.end() }; + + if (!ids_to_reserve.empty()) { + spdlog::info("Reserved ID(s) set to " + Join(ids_to_reserve)); + } + else { + spdlog::info("Cleared reserved ID(s)"); + } + + return ""; +} + +bool S2pExecutor::ValidateImageFile(const CommandContext& context, StorageDevice& storage_device, + const string& filename) const +{ +#ifdef BUILD_DISK + if (filename.empty()) { + return true; + } + + if (!CheckForReservedFile(context, filename)) { + return false; + } + + storage_device.SetFilename(filename); + + if (!StorageDevice::FileExists(filename)) { + // If the file does not exist search for it in the default image folder + const string effective_filename = context.GetDefaultFolder() + "/" + filename; + + if (!CheckForReservedFile(context, effective_filename)) { + return false; + } + + storage_device.SetFilename(effective_filename); + } + + try { + storage_device.Open(); + } + catch(const io_exception&) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_FILE_OPEN, storage_device.GetFilename()); + } +#endif + + return true; +} + +bool S2pExecutor::CheckForReservedFile(const CommandContext& context, const string& filename) +{ +#ifdef BUILD_DISK + if (const auto [id, lun] = StorageDevice::GetIdsForReservedFile(filename); id != -1) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_IMAGE_IN_USE, filename, + to_string(id) + ":" + to_string(lun)); + } +#endif + + return true; +} + +string S2pExecutor::PrintCommand(const PbCommand& command, const PbDeviceDefinition& pb_device) const +{ + const map> params = { command.params().begin(), command.params().end() }; + + ostringstream s; + s << "operation=" << PbOperation_Name(command.operation()); + + if (!params.empty()) { + s << ", command params="; + bool isFirst = true; + for (const auto& [key, value] : params) { + if (!isFirst) { + s << ", "; + } + isFirst = false; + string v = key != "token" ? value : "???"; + s << "'" << key << "=" << v << "'"; + } + } + + s << ", device=" << pb_device.id() << ":" << pb_device.unit() << ", type=" << PbDeviceType_Name(pb_device.type()); + + if (pb_device.params_size()) { + s << ", device params="; + bool isFirst = true; + for (const auto& [key, value]: pb_device.params()) { + if (!isFirst) { + s << ":"; + } + isFirst = false; + s << "'" << key << "=" << value << "'"; + } + } + + s << ", vendor='" << pb_device.vendor() << "', product='" << pb_device.product() + << "', revision='" << pb_device.revision() << "', block size=" << pb_device.block_size(); + + return s.str(); +} + +string S2pExecutor::EnsureLun0(const PbCommand& command) const +{ + // Mapping of available LUNs (bit vector) to devices + unordered_map luns; + + // Collect LUN bit vectors of new devices + for (const auto& device : command.devices()) { + luns[device.id()] |= 1 << device.unit(); + } + + // Collect LUN bit vectors of existing devices + for (const auto& device : Get_allDevices()) { + luns[device->GetId()] |= 1 << device->GetLun(); + } + + const auto& it = ranges::find_if_not(luns, [] (const auto& l) { return l.second & 0x01; } ); + return it == luns.end() ? "" : "LUN 0 is missing for device ID " + to_string((*it).first); +} + +bool S2pExecutor::VerifyExistingIdAndLun(const CommandContext& context, int id, int lun) const +{ + if (!controller_factory->HasController(id)) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_NON_EXISTING_DEVICE, to_string(id)); + } + + if (!controller_factory->HasDeviceForIdAndLun(id, lun)) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_NON_EXISTING_UNIT, to_string(id), to_string(lun)); + } + + return true; +} + +shared_ptr S2pExecutor::CreateDevice(const CommandContext& context, const PbDeviceType type, + int lun, const string& filename) const +{ + auto device = device_factory.CreateDevice(type, lun, filename); + if (!device) { + if (type == UNDEFINED) { + context.ReturnLocalizedError(LocalizationKey::ERROR_MISSING_DEVICE_TYPE, filename); + } + else { + context.ReturnLocalizedError(LocalizationKey::ERROR_UNKNOWN_DEVICE_TYPE, PbDeviceType_Name(type)); + } + + return nullptr; + } + + // Some device types must be unique + if (UNIQUE_DEVICE_TYPES.contains(device->GetType())) { + for (const auto& d : Get_allDevices()) { + if (d->GetType() == device->GetType()) { + context.ReturnLocalizedError(LocalizationKey::ERROR_UNIQUE_DEVICE_TYPE, + PbDeviceType_Name(device->GetType())); + return nullptr; + } + } + } + + return device; +} + +bool S2pExecutor::SetSectorSize(const CommandContext& context, shared_ptr device, int size) const +{ +#ifdef BUILD_DISK + if (size) { + const auto disk = dynamic_pointer_cast(device); + if (disk != nullptr && disk->IsSectorSizeConfigurable()) { + if (!disk->SetConfiguredSectorSize(size)) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_BLOCK_SIZE, to_string(size)); + } + } + else { + return context.ReturnLocalizedError(LocalizationKey::ERROR_BLOCK_SIZE_NOT_CONFIGURABLE, + device->GetTypeString()); + } + } +#endif + + return true; +} + +bool S2pExecutor::ValidateOperationAgainstDevice(const CommandContext& context, const PrimaryDevice& device, + PbOperation operation) +{ + if ((operation == START || operation == STOP) && !device.IsStoppable()) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION_DENIED_STOPPABLE, PbOperation_Name(operation), + device.GetTypeString()); + } + + if ((operation == INSERT || operation == EJECT) && !device.IsRemovable()) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION_DENIED_REMOVABLE, PbOperation_Name(operation), + device.GetTypeString()); + } + + if ((operation == PROTECT || operation == UNPROTECT) && !device.IsProtectable()) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION_DENIED_PROTECTABLE, PbOperation_Name(operation), + device.GetTypeString()); + } + + if ((operation == PROTECT || operation == UNPROTECT) && !device.IsReady()) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_OPERATION_DENIED_READY, PbOperation_Name(operation), + device.GetTypeString()); + } + + return true; +} + +bool S2pExecutor::ValidateIdAndLun(const CommandContext& context, int id, int lun) +{ + if (id < 0) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_MISSING_DEVICE_ID); + } + if (id >= ControllerFactory::GetIdMax()) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_INVALID_ID, to_string(id), + to_string(ControllerFactory::GetIdMax() - 1)); + } + if (lun < 0 || lun >= ControllerFactory::GetLunMax()) { + return context.ReturnLocalizedError(LocalizationKey::ERROR_INVALID_LUN, to_string(lun), + to_string(ControllerFactory::GetLunMax() - 1)); + } + + return true; +} + +bool S2pExecutor::SetProductData(const CommandContext& context, const PbDeviceDefinition& pb_device, + PrimaryDevice& device) +{ + try { + if (!pb_device.vendor().empty()) { + device.SetVendor(pb_device.vendor()); + } + if (!pb_device.product().empty()) { + device.SetProduct(pb_device.product()); + } + if (!pb_device.revision().empty()) { + device.SetRevision(pb_device.revision()); + } + } + catch(const invalid_argument& e) { + return context.ReturnErrorStatus(e.what()); + } + + return true; +} diff --git a/cpp/s2p/s2p_executor.h b/cpp/s2p/s2p_executor.h new file mode 100644 index 00000000..ac46c9ed --- /dev/null +++ b/cpp/s2p/s2p_executor.h @@ -0,0 +1,79 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "buses/bus.h" +#include "controllers/controller_factory.h" +#include "base/device_factory.h" +#include +#include + +class DeviceFactory; +class PrimaryDevice; +class StorageDevice; +class CommandContext; + +class S2pExecutor +{ +public: + + S2pExecutor(BUS& bus, shared_ptr controller_factory) + : bus(bus), controller_factory(controller_factory) {} + ~S2pExecutor() = default; + + // TODO At least some of these methods should be private, currently they are directly called by the unit tests + + auto GetReservedIds() const { return reserved_ids; } + + bool ProcessDeviceCmd(const CommandContext&, const PbDeviceDefinition&, bool); + bool ProcessCmd(const CommandContext&); + bool Start(PrimaryDevice&, bool) const; + bool Stop(PrimaryDevice&, bool) const; + bool Eject(PrimaryDevice&, bool) const; + bool Protect(PrimaryDevice&, bool) const; + bool Unprotect(PrimaryDevice&, bool) const; + bool Attach(const CommandContext&, const PbDeviceDefinition&, bool); + bool Insert(const CommandContext&, const PbDeviceDefinition&, const shared_ptr&, bool) const; + bool Detach(const CommandContext&, PrimaryDevice&, bool) const; + void DetachAll() const; + string SetReservedIds(string_view); + bool ValidateImageFile(const CommandContext&, StorageDevice&, const string&) const; + string PrintCommand(const PbCommand&, const PbDeviceDefinition&) const; + string EnsureLun0(const PbCommand&) const; + bool VerifyExistingIdAndLun(const CommandContext&, int, int) const; + shared_ptr CreateDevice(const CommandContext&, const PbDeviceType, int, const string&) const; + bool SetSectorSize(const CommandContext&, shared_ptr, int) const; + + static bool ValidateOperationAgainstDevice(const CommandContext&, const PrimaryDevice&, PbOperation); + static bool ValidateIdAndLun(const CommandContext&, int, int); + static bool SetProductData(const CommandContext&, const PbDeviceDefinition&, PrimaryDevice&); + + mutex& GetExecutionLocker() { return execution_locker; } + + auto Get_allDevices() const { return controller_factory->GetAllDevices(); } + +private: + + static bool CheckForReservedFile(const CommandContext&, const string&); + + BUS& bus; + + shared_ptr controller_factory; + + [[no_unique_address]] const DeviceFactory device_factory; + + mutex execution_locker; + + unordered_set reserved_ids; + + const inline static unordered_set UNIQUE_DEVICE_TYPES = { + PbDeviceType::SCDP, + PbDeviceType::SCHS + }; +}; diff --git a/cpp/s2p/s2p_image.cpp b/cpp/s2p/s2p_image.cpp new file mode 100644 index 00000000..6911c105 --- /dev/null +++ b/cpp/s2p/s2p_image.cpp @@ -0,0 +1,428 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "devices/disk.h" +#include "s2p_image.h" +#include "shared/protobuf_util.h" +#include +#include +#include +#include +#include + +using namespace std; +using namespace filesystem; +using namespace command_interface; +using namespace protobuf_util; + +S2pImage::S2pImage() +{ + // ~/images is the default folder for device image files, for the root user it is /home/pi/images + default_folder = GetHomeDir() + "/images"; +} + +bool S2pImage::CheckDepth(string_view filename) const +{ + return ranges::count(filename, '/') <= depth; +} + +bool S2pImage::CreateImageFolder(const CommandContext& context, string_view filename) const +{ + if (const auto folder = path(filename).parent_path(); !folder.string().empty()) { + // Checking for existence first prevents an error if the top-level folder is a softlink + if (error_code error; exists(folder, error)) { + return true; + } + + try { + create_directories(folder); + + return ChangeOwner(context, folder, false); + } + catch(const filesystem_error& e) { + return context.ReturnErrorStatus("Can't create image folder '" + folder.string() + "': " + e.what()); + } + } + + return true; +} + +string S2pImage::SetDefaultFolder(string_view f) +{ + if (f.empty()) { + return "Can't set default image folder: Missing folder name"; + } + + // If a relative path is specified, the path is assumed to be relative to the user's home directory + path folder(f); + if (folder.is_relative()) { + folder = path(GetHomeDir() + "/" + folder.string()); + } + + if (path home_root = path(GetHomeDir()).parent_path(); !folder.string().starts_with(home_root.string())) { + return "Default image folder must be located in '" + home_root.string() + "'"; + } + + // Resolve a potential symlink + if (error_code error; is_symlink(folder, error)) { + folder = read_symlink(folder); + } + + if (error_code error; !is_directory(folder)) { + return string("'") + folder.string() + "' is not a valid image folder"; + } + + default_folder = folder.string(); + + spdlog::info("Default image folder set to '" + default_folder + "'"); + + return ""; +} + +bool S2pImage::CreateImage(const CommandContext& context) const +{ + const string filename = GetParam(context.GetCommand(), "file"); + if (filename.empty()) { + return context.ReturnErrorStatus("Missing image filename"); + } + + if (!CheckDepth(filename)) { + return context.ReturnErrorStatus(("Invalid folder hierarchy depth '" + filename + "'").c_str()); + } + + const string full_filename = GetFullName(filename); + if (!IsValidDstFilename(full_filename)) { + return context.ReturnErrorStatus("Can't create image file: '" + full_filename + "': File already exists"); + } + + const string size = GetParam(context.GetCommand(), "size"); + if (size.empty()) { + return context.ReturnErrorStatus("Can't create image file '" + full_filename + "': Missing file size"); + } + + off_t len; + try { + len = stoull(size); + } + catch(const invalid_argument&) { + return context.ReturnErrorStatus("Can't create image file '" + full_filename + "': Invalid file size " + size); + } + catch(const out_of_range&) { + return context.ReturnErrorStatus("Can't create image file '" + full_filename + "': Invalid file size " + size); + } + if (len < 512 || (len & 0x1ff)) { + return context.ReturnErrorStatus("Invalid image file size " + to_string(len) + " (not a multiple of 512)"); + } + + if (!CreateImageFolder(context, full_filename)) { + return false; + } + + const bool read_only = GetParam(context.GetCommand(), "read_only") == "true"; + + error_code error; + path file(full_filename); + try { + ofstream s(file); + s.close(); + + if (!ChangeOwner(context, file, read_only)) { + return false; + } + + resize_file(file, len); + } + catch(const filesystem_error& e) { + filesystem::remove(file, error); + + return context.ReturnErrorStatus("Can't create image file '" + full_filename + "': " + e.what()); + } + + spdlog::info("Created " + string(read_only ? "read-only " : "") + "image file '" + full_filename + + "' with a size of " + to_string(len) + " bytes"); + + return context.ReturnSuccessStatus(); +} + +bool S2pImage::DeleteImage(const CommandContext& context) const +{ + const string filename = GetParam(context.GetCommand(), "file"); + if (filename.empty()) { + return context.ReturnErrorStatus("Missing image filename"); + } + + if (!CheckDepth(filename)) { + return context.ReturnErrorStatus("Invalid folder hierarchy depth '" + filename + "'"); + } + + const auto full_filename = path(GetFullName(filename)); + if (!exists(full_filename)) { + return context.ReturnErrorStatus("Image file '" + full_filename.string() + "' does not exist"); + } + + if (!IsReservedFile(context, full_filename, "delete")) { + return false; + } + + if (error_code error; !remove(full_filename, error)) { + return context.ReturnErrorStatus("Can't delete image file '" + full_filename.string() + "'"); + } + + // Delete empty subfolders + size_t last_slash = filename.rfind('/'); + while (last_slash != string::npos) { + const string folder = filename.substr(0, last_slash); + const auto full_folder = path(GetFullName(folder)); + + if (error_code error; !filesystem::is_empty(full_folder, error) || error) { + break; + } + + if (error_code error; !remove(full_folder)) { + return context.ReturnErrorStatus("Can't delete empty image folder '" + full_folder.string() + "'"); + } + + last_slash = folder.rfind('/'); + } + + spdlog::info("Deleted image file '" + full_filename.string() + "'"); + + return context.ReturnSuccessStatus(); +} + +bool S2pImage::RenameImage(const CommandContext& context) const +{ + string from; + string to; + if (!ValidateParams(context, "rename/move", from, to)) { + return false; + } + + try { + rename(path(from), path(to)); + } + catch(const filesystem_error& e) { + return context.ReturnErrorStatus("Can't rename/move image file '" + from + "': " + e.what()); + } + + spdlog::info("Renamed/Moved image file '" + from + "' to '" + to + "'"); + + return context.ReturnSuccessStatus(); +} + +bool S2pImage::CopyImage(const CommandContext& context) const +{ + string from; + string to; + if (!ValidateParams(context, "copy", from, to)) { + return false; + } + + path f(from); + path t(to); + + // Symbolic links need a special handling + if (error_code error; is_symlink(f, error)) { + try { + copy_symlink(f, t); + } + catch(const filesystem_error& e) { + return context.ReturnErrorStatus("Can't copy image file symlink '" + from + "': " + e.what()); + } + + spdlog::info("Copied image file symlink '" + from + "' to '" + to + "'"); + + return context.ReturnSuccessStatus(); + } + + try { + copy_file(f, t); + + permissions(t, GetParam(context.GetCommand(), "read_only") == "true" ? + perms::owner_read | perms::group_read | perms::others_read : + perms::owner_read | perms::group_read | perms::others_read | + perms::owner_write | perms::group_write); + } + catch(const filesystem_error& e) { + return context.ReturnErrorStatus("Can't copy image file '" + from + "': " + e.what()); + } + + spdlog::info("Copied image file '" + from + "' to '" + to + "'"); + + return context.ReturnSuccessStatus(); +} + +bool S2pImage::SetImagePermissions(const CommandContext& context) const +{ + const string filename = GetParam(context.GetCommand(), "file"); + if (filename.empty()) { + return context.ReturnErrorStatus("Missing image filename"); + } + + if (!CheckDepth(filename)) { + return context.ReturnErrorStatus("Invalid folder hierarchy depth '" + filename + "'"); + } + + const string full_filename = GetFullName(filename); + if (!IsValidSrcFilename(full_filename)) { + return context.ReturnErrorStatus("Can't modify image file '" + full_filename + "': Invalid name or type"); + } + + const bool protect = context.GetCommand().operation() == PROTECT_IMAGE; + + if (protect && !IsReservedFile(context, full_filename, "protect")) { + return false; + } + + try { + permissions(path(full_filename), protect ? + perms::owner_read | perms::group_read | perms::others_read : + perms::owner_read | perms::group_read | perms::others_read | + perms::owner_write | perms::group_write); + } + catch(const filesystem_error& e) { + return context.ReturnErrorStatus("Can't " + string(protect ? "protect" : "unprotect") + " image file '" + + full_filename + "': " + e.what()); + } + + spdlog::info((protect ? "Protected" : "Unprotected") + string(" image file '") + full_filename + "'"); + + return context.ReturnSuccessStatus(); +} + +bool S2pImage::IsReservedFile(const CommandContext& context, const string& file, const string& op) +{ +#ifdef BUILD_DISK + const auto [id, lun] = StorageDevice::GetIdsForReservedFile(file); + if (id != -1) { + return context.ReturnErrorStatus("Can't " + op + " image file '" + file + + "', it is currently being used by device " + to_string(id) + ":" + to_string(lun)); + } + + return true; +#else + return false; +#endif +} + +bool S2pImage::ValidateParams(const CommandContext& context, const string& op, string& from, string& to) const +{ + from = GetParam(context.GetCommand(), "from"); + if (from.empty()) { + return context.ReturnErrorStatus("Can't " + op + " image file: Missing source filename"); + } + + if (!CheckDepth(from)) { + return context.ReturnErrorStatus("Invalid folder hierarchy depth '" + from + "'"); + } + + to = GetParam(context.GetCommand(), "to"); + if (to.empty()) { + return context.ReturnErrorStatus("Can't " + op + " image file '" + from + "': Missing destination filename"); + } + + if (!CheckDepth(to)) { + return context.ReturnErrorStatus("Invalid folder hierarchy depth '" + to + "'"); + } + + from = GetFullName(from); + if (!IsValidSrcFilename(from)) { + return context.ReturnErrorStatus("Can't " + op + " image file: '" + from + "': Invalid name or type"); + } + + to = GetFullName(to); + if (!IsValidDstFilename(to)) { + return context.ReturnErrorStatus("Can't " + op + " image file '" + from + "' to '" + to + "': File already exists"); + } + + if (!IsReservedFile(context, from, op)) { + return false; + } + + if (!CreateImageFolder(context, to)) { + return false; + } + + return true; +} + +bool S2pImage::IsValidSrcFilename(string_view filename) +{ + // Source file must exist and must be a regular file or a symlink + path file(filename); + + error_code error; + return is_regular_file(file, error) || is_symlink(file, error); +} + +bool S2pImage::IsValidDstFilename(string_view filename) +{ + // Destination file must not yet exist + try { + return !exists(path(filename)); + } + catch(const filesystem_error&) { + return false; + } +} + +bool S2pImage::ChangeOwner(const CommandContext& context, const path& filename, bool read_only) +{ + const auto [uid, gid] = GetUidAndGid(); + if (chown(filename.c_str(), uid, gid)) { + // Remember the current error before the next filesystem operation + const int e = errno; + + error_code error; + remove(filename, error); + + return context.ReturnErrorStatus("Can't change ownership of '" + filename.string() + "': " + strerror(e)); + } + + permissions(filename, read_only ? + perms::owner_read | perms::group_read | perms::others_read : + perms::owner_read | perms::group_read | perms::others_read | + perms::owner_write | perms::group_write); + + return true; +} + +string S2pImage::GetHomeDir() +{ + const auto [uid, gid] = GetUidAndGid(); + + passwd pwd = {}; + passwd *p_pwd; + array pwbuf; + + if (uid && !getpwuid_r(uid, &pwd, pwbuf.data(), pwbuf.size(), &p_pwd)) { + return pwd.pw_dir; + } + else { + return "/home/pi"; + } +} + +pair S2pImage::GetUidAndGid() +{ + int uid = getuid(); + if (const char *sudo_user = getenv("SUDO_UID"); sudo_user) { + uid = stoi(sudo_user); + } + + passwd pwd = {}; + passwd *p_pwd; + array pwbuf; + + int gid = -1; + if (!getpwuid_r(uid, &pwd, pwbuf.data(), pwbuf.size(), &p_pwd)) { + gid = pwd.pw_gid; + } + + return { uid, gid }; +} diff --git a/cpp/s2p/s2p_image.h b/cpp/s2p/s2p_image.h new file mode 100644 index 00000000..ae179646 --- /dev/null +++ b/cpp/s2p/s2p_image.h @@ -0,0 +1,53 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "command_context.h" +#include +#include + +using namespace std; +using namespace filesystem; +using namespace command_interface; + +class S2pImage +{ +public: + + S2pImage(); + ~S2pImage() = default; + + void SetDepth(int d) { depth = d; } + int GetDepth() const { return depth; } + string GetDefaultFolder() const { return default_folder; } + string SetDefaultFolder(string_view); + bool CreateImage(const CommandContext&) const; + bool DeleteImage(const CommandContext&) const; + bool RenameImage(const CommandContext&) const; + bool CopyImage(const CommandContext&) const; + bool SetImagePermissions(const CommandContext&) const; + +private: + + bool CheckDepth(string_view) const; + string GetFullName(const string& filename) const { return default_folder + "/" + filename; } + bool CreateImageFolder(const CommandContext&, string_view) const; + static bool IsReservedFile(const CommandContext&, const string&, const string&); + bool ValidateParams(const CommandContext&, const string&, string&, string&) const; + + static bool IsValidSrcFilename(string_view); + static bool IsValidDstFilename(string_view); + static bool ChangeOwner(const CommandContext&, const path&, bool); + static string GetHomeDir(); + static pair GetUidAndGid(); + + string default_folder; + + int depth = 1; +}; diff --git a/cpp/s2p/s2p_response.cpp b/cpp/s2p/s2p_response.cpp new file mode 100644 index 00000000..3006c18e --- /dev/null +++ b/cpp/s2p/s2p_response.cpp @@ -0,0 +1,545 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "controllers/controller_factory.h" +#include "shared/protobuf_util.h" +#include "shared/network_util.h" +#include "shared/s2p_util.h" +#include "shared/s2p_version.h" +#include "devices/disk.h" +#include "s2p_response.h" +#include +#include + +using namespace std; +using namespace filesystem; +using namespace command_interface; +using namespace s2p_util; +using namespace network_util; +using namespace protobuf_util; + +void S2pResponse::GetDeviceProperties(shared_ptr device, PbDeviceProperties& properties) const +{ + properties.set_luns(ControllerFactory::GetScsiLunMax()); + properties.set_read_only(device->IsReadOnly()); + properties.set_protectable(device->IsProtectable()); + properties.set_stoppable(device->IsStoppable()); + properties.set_removable(device->IsRemovable()); + properties.set_lockable(device->IsLockable()); + properties.set_supports_file(device->SupportsFile()); + properties.set_supports_params(device->SupportsParams()); + + if (device->SupportsParams()) { + for (const auto& [key, value] : device->GetDefaultParams()) { + auto& map = *properties.mutable_default_params(); + map[key] = value; + } + } + +#ifdef BUILD_DISK + shared_ptr disk = dynamic_pointer_cast(device); + if (disk && disk->IsSectorSizeConfigurable()) { + for (const auto& sector_size : disk->GetSupportedSectorSizes()) { + properties.add_block_sizes(sector_size); + } + } +#endif +} + +void S2pResponse::GetDeviceTypeProperties(PbDeviceTypesInfo& device_types_info, PbDeviceType type) const +{ + auto type_properties = device_types_info.add_properties(); + type_properties->set_type(type); + const auto device = device_factory.CreateDevice(type, 0, ""); + GetDeviceProperties(device, *type_properties->mutable_properties()); +} + +void S2pResponse::GetDeviceTypesInfo(PbDeviceTypesInfo& device_types_info) const +{ + int ordinal = 1; + while (PbDeviceType_IsValid(ordinal)) { + PbDeviceType type = UNDEFINED; + PbDeviceType_Parse(PbDeviceType_Name((PbDeviceType)ordinal), &type); + // Only report device types actually supported by the factory + if (device_factory.CreateDevice(type, 0, "")) { + GetDeviceTypeProperties(device_types_info, type); + } + ordinal++; + } +} + +void S2pResponse::GetDevice(shared_ptr device, PbDevice& pb_device, const string& default_folder) const +{ + pb_device.set_id(device->GetId()); + pb_device.set_unit(device->GetLun()); + pb_device.set_vendor(device->GetVendor()); + pb_device.set_product(device->GetProduct()); + pb_device.set_revision(device->GetRevision()); + pb_device.set_type(device->GetType()); + + GetDeviceProperties(device, *pb_device.mutable_properties()); + + auto status = pb_device.mutable_status(); + status->set_protected_(device->IsProtected()); + status->set_stopped(device->IsStopped()); + status->set_removed(device->IsRemoved()); + status->set_locked(device->IsLocked()); + + if (device->SupportsParams()) { + for (const auto& [key, value] : device->GetParams()) { + SetParam(pb_device, key, value); + } + } + +#ifdef BUILD_DISK + if (const auto disk = dynamic_pointer_cast(device); disk) { + pb_device.set_block_size(device->IsRemoved()? 0 : disk->GetSectorSizeInBytes()); + pb_device.set_block_count(device->IsRemoved() ? 0: disk->GetBlockCount()); + } + + const auto storage_device = dynamic_pointer_cast(device); + if (storage_device) { + GetImageFile(*pb_device.mutable_file(), default_folder, device->IsReady() ? storage_device->GetFilename() : ""); + } +#endif +} + +bool S2pResponse::GetImageFile(PbImageFile& image_file, const string& default_folder, const string& filename) const +{ + if (!filename.empty()) { + image_file.set_name(filename); + image_file.set_type(device_factory.GetTypeForFile(filename)); + + const path p(filename[0] == '/' ? filename : default_folder + "/" + filename); + + image_file.set_read_only(access(p.c_str(), W_OK)); + + error_code error; + if (is_regular_file(p, error) || (is_symlink(p, error) && !is_block_file(p, error))) { + image_file.set_size(file_size(p)); + return true; + } + } + + return false; +} + +void S2pResponse::GetAvailableImages(PbImageFilesInfo& image_files_info, const string& default_folder, + const string& folder_pattern, const string& file_pattern, int scan_depth) const +{ + const path default_path(default_folder); + if (!is_directory(default_path)) { + return; + } + + string folder_pattern_lower; + ranges::transform(folder_pattern, back_inserter(folder_pattern_lower), ::tolower); + + string file_pattern_lower; + ranges::transform(file_pattern, back_inserter(file_pattern_lower), ::tolower); + + for (auto it = recursive_directory_iterator(default_path, directory_options::follow_directory_symlink); + it != recursive_directory_iterator(); it++) { + if (it.depth() > scan_depth) { + it.disable_recursion_pending(); + continue; + } + + const string parent = it->path().parent_path().string(); + + const string folder = parent.size() > default_folder.size() ? parent.substr(default_folder.size() + 1) : ""; + + if (!FilterMatches(folder, folder_pattern_lower) || !FilterMatches(it->path().filename().string(), file_pattern_lower)) { + continue; + } + + if (!ValidateImageFile(it->path())) { + continue; + } + + const string filename = folder.empty() ? + it->path().filename().string() : folder + "/" + it->path().filename().string(); + if (PbImageFile image_file; GetImageFile(image_file, default_folder, filename)) { + GetImageFile(*image_files_info.add_image_files(), default_folder, filename); + } + } +} + +void S2pResponse::GetImageFilesInfo(PbImageFilesInfo& image_files_info, const string& default_folder, + const string& folder_pattern, const string& file_pattern, int scan_depth) const +{ + image_files_info.set_default_image_folder(default_folder); + image_files_info.set_depth(scan_depth); + + GetAvailableImages(image_files_info, default_folder, folder_pattern, file_pattern, scan_depth); +} + +void S2pResponse::GetAvailableImages(PbServerInfo& server_info, const string& default_folder, + const string& folder_pattern, const string& file_pattern, int scan_depth) const +{ + server_info.mutable_image_files_info()->set_default_image_folder(default_folder); + + GetImageFilesInfo(*server_info.mutable_image_files_info(), default_folder, folder_pattern, file_pattern, scan_depth); +} + +void S2pResponse::GetReservedIds(PbReservedIdsInfo& reserved_ids_info, const unordered_set& ids) const +{ + for (const int id : ids) { + reserved_ids_info.add_ids(id); + } +} + +void S2pResponse::GetDevices(const unordered_set>& devices, PbServerInfo& server_info, + const string& default_folder) const +{ + for (const auto& device : devices) { + PbDevice *pb_device = server_info.mutable_devices_info()->add_devices(); + GetDevice(device, *pb_device, default_folder); + } +} + +void S2pResponse::GetDevicesInfo(const unordered_set>& devices, PbResult& result, + const PbCommand& command, const string& default_folder) const +{ + set id_sets; + + // If no device list was provided in the command get information on all devices + if (!command.devices_size()) { + for (const auto& device : devices) { + id_sets.insert({ device->GetId(), device->GetLun() }); + } + } + // Otherwise get information on the devices provided in the command + else { + id_sets = MatchDevices(devices, result, command); + if (id_sets.empty()) { + return; + } + } + + auto devices_info = result.mutable_devices_info(); + for (const auto& [id, lun] : id_sets) { + for (const auto& d : devices) { + if (d->GetId() == id && d->GetLun() == lun) { + GetDevice(d, *devices_info->add_devices(), default_folder); + break; + } + } + } + + result.set_status(true); +} + +void S2pResponse::GetServerInfo(PbServerInfo& server_info, const PbCommand& command, + const unordered_set>& devices, const unordered_set& reserved_ids, + const string& default_folder, int scan_depth) const +{ + const vector command_operations = Split(GetParam(command, "operations"), ','); + set> operations; + for (const string& operation : command_operations) { + string op; + ranges::transform(operation, back_inserter(op), ::toupper); + operations.insert(op); + } + + if (!operations.empty()) { + spdlog::trace("Requested operation(s): " + Join(operations, ",")); + } + + if (HasOperation(operations, PbOperation::VERSION_INFO)) { + GetVersionInfo(*server_info.mutable_version_info()); + } + + if (HasOperation(operations, PbOperation::LOG_LEVEL_INFO)) { + GetLogLevelInfo(*server_info.mutable_log_level_info()); + } + + if (HasOperation(operations, PbOperation::DEVICE_TYPES_INFO)) { + GetDeviceTypesInfo(*server_info.mutable_device_types_info()); + } + + if (HasOperation(operations, PbOperation::DEFAULT_IMAGE_FILES_INFO)) { + GetAvailableImages(server_info, default_folder, GetParam(command, "folder_pattern"), + GetParam(command, "file_pattern"), scan_depth); + } + + if (HasOperation(operations, PbOperation::NETWORK_INTERFACES_INFO)) { + GetNetworkInterfacesInfo(*server_info.mutable_network_interfaces_info()); + } + + if (HasOperation(operations, PbOperation::MAPPING_INFO)) { + GetMappingInfo(*server_info.mutable_mapping_info()); + } + + if (HasOperation(operations, PbOperation::STATISTICS_INFO)) { + GetStatisticsInfo(*server_info.mutable_statistics_info(), devices); + } + + if (HasOperation(operations, PbOperation::DEVICES_INFO)) { + GetDevices(devices, server_info, default_folder); + } + + if (HasOperation(operations, PbOperation::RESERVED_IDS_INFO)) { + GetReservedIds(*server_info.mutable_reserved_ids_info(), reserved_ids); + } + + if (HasOperation(operations, PbOperation::OPERATION_INFO)) { + GetOperationInfo(*server_info.mutable_operation_info(), scan_depth); + } +} + +void S2pResponse::GetVersionInfo(PbVersionInfo& version_info) const +{ + version_info.set_major_version(s2p_major_version); + version_info.set_minor_version(s2p_minor_version); + version_info.set_patch_version(s2p_patch_version); + version_info.set_identifier("SCSI2Pi"); +} + +void S2pResponse::GetLogLevelInfo(PbLogLevelInfo& log_level_info) const +{ + for (const auto& log_level : spdlog::level::level_string_views) { + log_level_info.add_log_levels(log_level.data()); + } + + log_level_info.set_current_log_level(spdlog::level::level_string_views[spdlog::get_level()].data()); +} + +void S2pResponse::GetNetworkInterfacesInfo(PbNetworkInterfacesInfo& network_interfaces_info) const +{ + for (const auto& network_interface : GetNetworkInterfaces()) { + network_interfaces_info.add_name(network_interface); + } +} + +void S2pResponse::GetMappingInfo(PbMappingInfo& mapping_info) const +{ + for (const auto& [name, type] : device_factory.GetExtensionMapping()) { + (*mapping_info.mutable_mapping())[name] = type; + } +} + +void S2pResponse::GetStatisticsInfo(PbStatisticsInfo& statistics_info, + const unordered_set>& devices) const +{ + for (const auto& device : devices) { + for (const auto& statistics : device->GetStatistics()) { + auto s = statistics_info.add_statistics(); + s->set_id(statistics.id()); + s->set_unit(statistics.unit()); + s->set_category(statistics.category()); + s->set_key(statistics.key()); + s->set_value(statistics.value()); + } + } +} + +void S2pResponse::GetOperationInfo(PbOperationInfo& operation_info, int depth) const +{ + auto operation = CreateOperation(operation_info, ATTACH, "Attach device, device-specific parameters are required"); + AddOperationParameter(*operation, "name", "Image file name in case of a mass storage device"); + AddOperationParameter(*operation, "interface", "Comma-separated prioritized network interface list"); + AddOperationParameter(*operation, "inet", "IP address and netmask of the network bridge"); + AddOperationParameter(*operation, "cmd", "Print command for the printer device"); + + CreateOperation(operation_info, DETACH, "Detach device, device-specific parameters are required"); + + CreateOperation(operation_info, DETACH_ALL, "Detach all devices"); + + CreateOperation(operation_info, START, "Start device, device-specific parameters are required"); + + CreateOperation(operation_info, STOP, "Stop device, device-specific parameters are required"); + + operation = CreateOperation(operation_info, INSERT, "Insert medium, device-specific parameters are required"); + AddOperationParameter(*operation, "file", "Image file name", "", true); + + CreateOperation(operation_info, EJECT, "Eject medium, device-specific parameters are required"); + + CreateOperation(operation_info, PROTECT, "Protect medium, device-specific parameters are required"); + + CreateOperation(operation_info, UNPROTECT, "Unprotect medium, device-specific parameters are required"); + + operation = CreateOperation(operation_info, SERVER_INFO, "Get server information"); + if (depth) { + AddOperationParameter(*operation, "folder_pattern", "Pattern for filtering image folder names"); + } + AddOperationParameter(*operation, "file_pattern", "Pattern for filtering image file names"); + + CreateOperation(operation_info, VERSION_INFO, "Get piscsi server version"); + + CreateOperation(operation_info, DEVICES_INFO, "Get information on attached devices"); + + CreateOperation(operation_info, DEVICE_TYPES_INFO, "Get device properties by device type"); + + operation = CreateOperation(operation_info, DEFAULT_IMAGE_FILES_INFO, "Get information on available image files"); + if (depth) { + AddOperationParameter(*operation, "folder_pattern", "Pattern for filtering image folder names"); + } + AddOperationParameter(*operation, "file_pattern", "Pattern for filtering image file names"); + + operation = CreateOperation(operation_info, IMAGE_FILE_INFO, "Get information on image file"); + AddOperationParameter(*operation, "file", "Image file name", "", true); + + CreateOperation(operation_info, LOG_LEVEL_INFO, "Get log level information"); + + CreateOperation(operation_info, NETWORK_INTERFACES_INFO, "Get the available network interfaces"); + + CreateOperation(operation_info, MAPPING_INFO, "Get mapping of extensions to device types"); + + CreateOperation(operation_info, STATISTICS_INFO, "Get statistics"); + + CreateOperation(operation_info, RESERVED_IDS_INFO, "Get list of reserved device IDs"); + + operation = CreateOperation(operation_info, DEFAULT_FOLDER, "Set default image file folder"); + AddOperationParameter(*operation, "folder", "Default image file folder name", "", true); + + operation = CreateOperation(operation_info, LOG_LEVEL, "Set log level"); + AddOperationParameter(*operation, "level", "New log level", "", true); + + operation = CreateOperation(operation_info, RESERVE_IDS, "Reserve device IDs"); + AddOperationParameter(*operation, "ids", "Comma-separated device ID list", "", true); + + operation = CreateOperation(operation_info, SHUT_DOWN, "Shut down or reboot"); + if (getuid()) { + AddOperationParameter(*operation, "mode", "Shutdown mode", "", true, { "rascsi" } ); + } + else { + // System shutdown/reboot requires root permissions + AddOperationParameter(*operation, "mode", "Shutdown mode", "", true, { "rascsi", "system", "reboot" } ); + } + + operation = CreateOperation(operation_info, CREATE_IMAGE, "Create an image file"); + AddOperationParameter(*operation, "file", "Image file name", "", true); + AddOperationParameter(*operation, "size", "Image file size in bytes", "", true); + AddOperationParameter(*operation, "read_only", "Read-only flag", "false", false, { "true", "false" } ); + + operation = CreateOperation(operation_info, DELETE_IMAGE, "Delete image file"); + AddOperationParameter(*operation, "file", "Image file name", "", true); + + operation = CreateOperation(operation_info, RENAME_IMAGE, "Rename image file"); + AddOperationParameter(*operation, "from", "Source image file name", "", true); + AddOperationParameter(*operation, "to", "Destination image file name", "", true); + + operation = CreateOperation(operation_info, COPY_IMAGE, "Copy image file"); + AddOperationParameter(*operation, "from", "Source image file name", "", true); + AddOperationParameter(*operation, "to", "Destination image file name", "", true); + AddOperationParameter(*operation, "read_only", "Read-only flag", "false", false, { "true", "false" } ); + + operation = CreateOperation(operation_info, PROTECT_IMAGE, "Write-protect image file"); + AddOperationParameter(*operation, "file", "Image file name", "", true); + + operation = CreateOperation(operation_info, UNPROTECT_IMAGE, "Make image file writable"); + AddOperationParameter(*operation, "file", "Image file name", "", true); + + operation = CreateOperation(operation_info, CHECK_AUTHENTICATION, "Check whether an authentication token is valid"); + AddOperationParameter(*operation, "token", "Authentication token to be checked", "", true); + + CreateOperation(operation_info, OPERATION_INFO, "Get operation meta data"); +} + +// This method returns a raw pointer because protobuf does not have support for smart pointers +PbOperationMetaData *S2pResponse::CreateOperation(PbOperationInfo& operation_info, const PbOperation& operation, + const string& description) const +{ + PbOperationMetaData meta_data; + meta_data.set_server_side_name(PbOperation_Name(operation)); + meta_data.set_description(description); + int ordinal = PbOperation_descriptor()->FindValueByName(PbOperation_Name(operation))->index(); + (*operation_info.mutable_operations())[ordinal] = meta_data; + return &(*operation_info.mutable_operations())[ordinal]; +} + +void S2pResponse::AddOperationParameter(PbOperationMetaData& meta_data, const string& name, + const string& description, const string& default_value, bool is_mandatory, + const vector& permitted_values) const +{ + auto parameter = meta_data.add_parameters(); + parameter->set_name(name); + parameter->set_description(description); + parameter->set_default_value(default_value); + parameter->set_is_mandatory(is_mandatory); + for (const auto& permitted_value : permitted_values) { + parameter->add_permitted_values(permitted_value); + } +} + +set S2pResponse::MatchDevices(const unordered_set>& devices, PbResult& result, + const PbCommand& command) const +{ + set id_sets; + + for (const auto& device : command.devices()) { + bool has_device = false; + for (const auto& d : devices) { + if (d->GetId() == device.id() && d->GetLun() == device.unit()) { + id_sets.insert({ device.id(), device.unit() }); + has_device = true; + break; + } + } + + if (!has_device) { + id_sets.clear(); + + result.set_status(false); + result.set_msg("No device for " + to_string(device.id()) + ":" + to_string(device.unit())); + + break; + } + } + + return id_sets; +} + +bool S2pResponse::ValidateImageFile(const path& path) +{ + if (path.filename().string().starts_with(".")) { + return false; + } + + filesystem::path p(path); + + // Follow symlink + if (is_symlink(p)) { + p = read_symlink(p); + if (!exists(p)) { + spdlog::warn("Image file symlink '" + path.string() + "' is broken"); + return false; + } + } + + if (is_directory(p) || (is_other(p) && !is_block_file(p))) { + return false; + } + + if (!is_block_file(p) && file_size(p) < 256) { + spdlog::warn("Image file '" + p.string() + "' is invalid"); + return false; + } + + return true; +} + +bool S2pResponse::FilterMatches(const string& input, string_view pattern_lower) +{ + if (!pattern_lower.empty()) { + string name_lower; + ranges::transform(input, back_inserter(name_lower), ::tolower); + + if (name_lower.find(pattern_lower) == string::npos) { + return false; + } + } + + return true; +} + +bool S2pResponse::HasOperation(const set>& operations, PbOperation operation) +{ + return operations.empty() || operations.contains(PbOperation_Name(operation)); +} diff --git a/cpp/s2p/s2p_response.h b/cpp/s2p/s2p_response.h new file mode 100644 index 00000000..0a20f7cc --- /dev/null +++ b/cpp/s2p/s2p_response.h @@ -0,0 +1,65 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "base/device_factory.h" +#include "base/primary_device.h" +#include "shared/s2p_util.h" +#include "generated/command_interface.pb.h" +#include +#include + +using namespace std; +using namespace filesystem; +using namespace command_interface; + +class S2pResponse +{ +public: + + S2pResponse() { } //NOSONAR Required by the bookworm compiler + ~S2pResponse() = default; + + bool GetImageFile(PbImageFile&, const string&, const string&) const; + void GetImageFilesInfo(PbImageFilesInfo&, const string&, const string&, const string&, int) const; + void GetReservedIds(PbReservedIdsInfo&, const unordered_set&) const; + void GetDevices(const unordered_set>&, PbServerInfo&, const string&) const; + void GetDevicesInfo(const unordered_set>&, PbResult&, const PbCommand&, const string&) const; + void GetDeviceTypesInfo(PbDeviceTypesInfo&) const; + void GetVersionInfo(PbVersionInfo&) const; + void GetServerInfo(PbServerInfo&, const PbCommand&, const unordered_set>&, + const unordered_set&, const string&, int) const; + void GetNetworkInterfacesInfo(PbNetworkInterfacesInfo&) const; + void GetMappingInfo(PbMappingInfo&) const; + void GetLogLevelInfo(PbLogLevelInfo&) const; + void GetStatisticsInfo(PbStatisticsInfo&, const unordered_set>&) const; + void GetOperationInfo(PbOperationInfo&, int) const; + +private: + + inline static const vector EMPTY_VECTOR; + + [[no_unique_address]] const DeviceFactory device_factory; + + void GetDeviceProperties(shared_ptr, PbDeviceProperties&) const; + void GetDevice(shared_ptr, PbDevice&, const string&) const; + void GetDeviceTypeProperties(PbDeviceTypesInfo&, PbDeviceType) const; + void GetAvailableImages(PbImageFilesInfo&, const string&, const string&, const string&, int) const; + void GetAvailableImages(PbServerInfo&, const string&, const string&, const string&, int) const; + PbOperationMetaData *CreateOperation(PbOperationInfo&, const PbOperation&, const string&) const; + void AddOperationParameter(PbOperationMetaData&, const string&, const string&, + const string& = "", bool = false, const vector& = EMPTY_VECTOR) const; + set MatchDevices(const unordered_set>&, PbResult&, const PbCommand&) const; + + static bool ValidateImageFile(const path&); + + static bool FilterMatches(const string&, string_view); + + static bool HasOperation(const set>&, PbOperation); +}; diff --git a/cpp/s2p/s2p_service.cpp b/cpp/s2p/s2p_service.cpp new file mode 100644 index 00000000..2eca5747 --- /dev/null +++ b/cpp/s2p/s2p_service.cpp @@ -0,0 +1,114 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/s2p_util.h" +#include "shared/shared_exceptions.h" +#include "command_context.h" +#include "s2p_service.h" +#include +#include +#include +#include +#include +#include + +using namespace s2p_util; + +string S2pService::Init(const callback& cb, int port) +{ + assert(service_socket == -1); + + if (port <= 0 || port > 65535) { + return "Invalid port number " + to_string(port); + } + + service_socket = socket(PF_INET, SOCK_STREAM, 0); + if (service_socket == -1) { + return "Unable to create service socket: " + string(strerror(errno)); + } + + if (const int yes = 1; setsockopt(service_socket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) { + Stop(); + return "Can't reuse address"; + } + + sockaddr_in server = {}; + server.sin_family = PF_INET; + server.sin_port = htons((uint16_t)port); + server.sin_addr.s_addr = INADDR_ANY; + if (bind(service_socket, reinterpret_cast(&server), //NOSONAR bit_cast is not supported by the bullseye compiler + static_cast(sizeof(sockaddr_in))) < 0) { + Stop(); + return "Port " + to_string(port) + " is in use, s2p may already be running"; + } + + if (listen(service_socket, 2) == -1) { + Stop(); + return "Can't listen to service socket: " + string(strerror(errno)); + } + + execute = cb; + + return ""; +} + +void S2pService::Start() +{ + assert(service_socket != -1); + +#if !defined __FreeBSD__ && !defined __APPLE__ + service_thread = jthread([this] () { Execute(); } ); +#else + service_thread = thread([this] () { Execute(); } ); +#endif +} + +void S2pService::Stop() +{ + // This method might be called twice when pressing Ctrl-C, because of the installed handlers + if (service_socket != -1) { + shutdown(service_socket, SHUT_RD); + close(service_socket); + + service_socket = -1; + } +} + +void S2pService::Execute() const +{ + while (service_socket != -1) { + const int fd = accept(service_socket, nullptr, nullptr); + if (fd != -1) { + ExecuteCommand(fd); + close(fd); + } + } +} + +void S2pService::ExecuteCommand(int fd) const +{ + CommandContext context(fd); + try { + if (context.ReadCommand()) { + execute(context); + } + } + catch(const io_exception& e) { + spdlog::warn(e.what()); + + // Try to return an error message (this may fail if the exception was caused when returning the actual result) + PbResult result; + result.set_msg(e.what()); + try { + context.WriteResult(result); + } + catch(const io_exception&) { //NOSONAR Ignored because not relevant when writing the result + // Ignore + } + } +} diff --git a/cpp/s2p/s2p_service.h b/cpp/s2p/s2p_service.h new file mode 100644 index 00000000..65bd686f --- /dev/null +++ b/cpp/s2p/s2p_service.h @@ -0,0 +1,47 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include +#include +#include + +class CommandContext; + +using namespace std; + +class S2pService +{ + using callback = function; + +public: + + S2pService() = default; + ~S2pService() = default; + + string Init(const callback&, int); + void Start(); + void Stop(); + bool IsRunning() const { return service_socket != -1 && service_thread.joinable(); } + +private: + + void Execute() const; + void ExecuteCommand(int) const; + + callback execute; + +#if !defined __FreeBSD__ && !defined __APPLE__ + jthread service_thread; +#else + thread service_thread; +#endif + + int service_socket = -1; +}; diff --git a/cpp/s2pctl/s2pctl.cpp b/cpp/s2pctl/s2pctl.cpp new file mode 100644 index 00000000..048f933a --- /dev/null +++ b/cpp/s2pctl/s2pctl.cpp @@ -0,0 +1,18 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "s2pctl/s2pctl_core.h" + +using namespace std; + +int main(int argc, char *argv[]) +{ + const vector args(argv, argv + argc); + + return ScsiCtl().run(args); +} diff --git a/cpp/s2pctl/s2pctl_commands.cpp b/cpp/s2pctl/s2pctl_commands.cpp new file mode 100644 index 00000000..99e6b1d2 --- /dev/null +++ b/cpp/s2pctl/s2pctl_commands.cpp @@ -0,0 +1,442 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/network_util.h" +#include "shared/s2p_util.h" +#include "shared/protobuf_util.h" +#include "shared/shared_exceptions.h" +#include "s2pctl_commands.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace google::protobuf; +using namespace google::protobuf::util; +using namespace command_interface; +using namespace network_util; +using namespace s2p_util; +using namespace protobuf_util; + +bool S2pCtlCommands::Execute(string_view log_level, string_view default_folder, string_view reserved_ids, + string_view image_params, string_view filename) +{ + switch(command.operation()) { + case LOG_LEVEL: + return CommandLogLevel(log_level); + + case DEFAULT_FOLDER: + return CommandDefaultImageFolder(default_folder); + + case RESERVE_IDS: + return CommandReserveIds(reserved_ids); + + case CREATE_IMAGE: + return CommandCreateImage(image_params); + + case DELETE_IMAGE: + return CommandDeleteImage(image_params); + + case RENAME_IMAGE: + return CommandRenameImage(image_params); + + case COPY_IMAGE: + return CommandCopyImage(image_params); + + case DEVICES_INFO: + return CommandDeviceInfo(); + + case DEVICE_TYPES_INFO: + return CommandDeviceTypesInfo(); + + case VERSION_INFO: + return CommandVersionInfo(); + + case SERVER_INFO: + return CommandServerInfo(); + + case DEFAULT_IMAGE_FILES_INFO: + return CommandDefaultImageFilesInfo(); + + case IMAGE_FILE_INFO: + return CommandImageFileInfo(filename); + + case NETWORK_INTERFACES_INFO: + return CommandNetworkInterfacesInfo(); + + case LOG_LEVEL_INFO: + return CommandLogLevelInfo(); + + case RESERVED_IDS_INFO: + return CommandReservedIdsInfo(); + + case MAPPING_INFO: + return CommandMappingInfo(); + + case STATISTICS_INFO: + return CommandStatisticsInfo(); + + case OPERATION_INFO: + return CommandOperationInfo(); + + case NO_OPERATION: + return false; + + default: + return SendCommand(); + } + + return false; +} + +bool S2pCtlCommands::SendCommand() +{ + if (!filename_binary.empty()) { + ExportAsBinary(command, filename_binary); + } + if (!filename_json.empty()) { + ExportAsJson(command, filename_json); + } + if (!filename_text.empty()) { + ExportAsText(command, filename_text); + } + + // Do not execute the command when the command data are exported + if (!filename_binary.empty() || !filename_json.empty() || !filename_text.empty()) { + return true; + } + + sockaddr_in server_addr = {}; + if (!ResolveHostName(hostname, &server_addr)) { + throw io_exception("Can't resolve hostname '" + hostname + "'"); + } + + const int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd == -1) { + throw io_exception("Can't create socket: " + string(strerror(errno))); + } + + server_addr.sin_port = htons(uint16_t(port)); + if (connect(fd, (sockaddr *)&server_addr, sizeof(server_addr)) < 0) { + close(fd); + + throw io_exception("Can't connect to s2p on host '" + hostname + "', port " + to_string(port) + + ": " + strerror(errno)); + } + + if (write(fd, "RASCSI", 6) != 6) { + close(fd); + + throw io_exception("Can't write magic"); + } + + SerializeMessage(fd, command); + DeserializeMessage(fd, result); + + close(fd); + + if (!result.status()) { + throw io_exception(result.msg()); + } + + if (!result.msg().empty()) { + cout << result.msg() << endl; + } + + return true; +} + +bool S2pCtlCommands::CommandDevicesInfo() +{ + SendCommand(); + + cout << s2pctl_display.DisplayDevicesInfo(result.devices_info()) << flush; + + return true; +} + +bool S2pCtlCommands::CommandLogLevel(string_view log_level) +{ + SetParam(command, "level", log_level); + + return SendCommand(); +} + +bool S2pCtlCommands::CommandReserveIds(string_view reserved_ids) +{ + SetParam(command, "ids", reserved_ids); + + return SendCommand(); +} + +bool S2pCtlCommands::CommandCreateImage(string_view image_params) +{ + if (!EvaluateParams(image_params, "file", "size")) { + cerr << "Error: Invalid file descriptor '" << image_params << "', format is NAME:SIZE" << endl; + + return false; + } + + SetParam(command, "read_only", "false"); + + return SendCommand(); +} + +bool S2pCtlCommands::CommandDeleteImage(string_view filename) +{ + SetParam(command, "file", filename); + + return SendCommand(); +} + +bool S2pCtlCommands::CommandRenameImage(string_view image_params) +{ + if (!EvaluateParams(image_params, "from", "to")) { + cerr << "Error: Invalid file descriptor '" << image_params << "', format is CURRENT_NAME:NEW_NAME" << endl; + + return false; + } + + return SendCommand(); +} + +bool S2pCtlCommands::CommandCopyImage(string_view image_params) +{ + if (!EvaluateParams(image_params, "from", "to")) { + cerr << "Error: Invalid file descriptor '" << image_params << "', format is CURRENT_NAME:NEW_NAME" << endl; + + return false; + } + + return SendCommand(); +} + +bool S2pCtlCommands::CommandDefaultImageFolder(string_view folder) +{ + SetParam(command, "folder", folder); + + return SendCommand(); +} + +bool S2pCtlCommands::CommandDeviceInfo() +{ + SendCommand(); + + for (const auto& device : result.devices_info().devices()) { + cout << s2pctl_display.DisplayDeviceInfo(device); + } + + cout << flush; + + return true; +} + +bool S2pCtlCommands::CommandDeviceTypesInfo() +{ + SendCommand(); + + cout << s2pctl_display.DisplayDeviceTypesInfo(result.device_types_info()) << flush; + + return true; +} + +bool S2pCtlCommands::CommandVersionInfo() +{ + SendCommand(); + + cout << s2pctl_display.DisplayVersionInfo(result.version_info()) << flush; + + return true; +} + +bool S2pCtlCommands::CommandServerInfo() +{ + SendCommand(); + + PbServerInfo server_info = result.server_info(); + + if (server_info.has_version_info()) { + cout << s2pctl_display.DisplayVersionInfo(server_info.version_info()); + } + + if (server_info.has_log_level_info()) { + cout << s2pctl_display.DisplayLogLevelInfo(server_info.log_level_info()); + } + + if (server_info.has_image_files_info()) { + cout << s2pctl_display.DisplayImageFilesInfo(server_info.image_files_info()); + } + + if (server_info.has_mapping_info()) { + cout << s2pctl_display.DisplayMappingInfo(server_info.mapping_info()); + } + + if (server_info.has_network_interfaces_info()) { + cout << s2pctl_display.DisplayNetworkInterfaces(server_info.network_interfaces_info()); + } + + if (server_info.has_device_types_info()) { + cout << s2pctl_display.DisplayDeviceTypesInfo(server_info.device_types_info()); + } + + if (server_info.has_reserved_ids_info()) { + cout << s2pctl_display.DisplayReservedIdsInfo(server_info.reserved_ids_info()); + } + + if (server_info.has_statistics_info()) { + cout << s2pctl_display.DisplayStatisticsInfo(server_info.statistics_info()); + } + + if (server_info.has_operation_info()) { + cout << s2pctl_display.DisplayOperationInfo(server_info.operation_info()); + } + + if (server_info.has_devices_info() && server_info.devices_info().devices_size()) { + vector sorted_devices = { server_info.devices_info().devices().begin(), server_info.devices_info().devices().end() }; + ranges::sort(sorted_devices, [](const auto& a, const auto& b) { return a.id() < b.id() || a.unit() < b.unit(); }); + + cout << "Attached devices:\n"; + + for (const auto& device : sorted_devices) { + cout << s2pctl_display.DisplayDeviceInfo(device); + } + } + + cout << flush; + + return true; +} + +bool S2pCtlCommands::CommandDefaultImageFilesInfo() +{ + SendCommand(); + + cout << s2pctl_display.DisplayImageFilesInfo(result.image_files_info()) << flush; + + return true; +} + +bool S2pCtlCommands::CommandImageFileInfo(string_view filename) +{ + SetParam(command, "file", filename); + + SendCommand(); + + cout << s2pctl_display.DisplayImageFile(result.image_file_info()) << flush; + + return true; +} + +bool S2pCtlCommands::CommandNetworkInterfacesInfo() +{ + SendCommand(); + + cout << s2pctl_display.DisplayNetworkInterfaces(result.network_interfaces_info()) << flush; + + return true; +} + +bool S2pCtlCommands::CommandLogLevelInfo() +{ + SendCommand(); + + cout << s2pctl_display.DisplayLogLevelInfo(result.log_level_info()) << flush; + + return true; +} + +bool S2pCtlCommands::CommandReservedIdsInfo() +{ + SendCommand(); + + cout << s2pctl_display.DisplayReservedIdsInfo(result.reserved_ids_info()) << flush; + + return true; +} + +bool S2pCtlCommands::CommandMappingInfo() +{ + SendCommand(); + + cout << s2pctl_display.DisplayMappingInfo(result.mapping_info()) << flush; + + return true; +} + +bool S2pCtlCommands::CommandStatisticsInfo() +{ + SendCommand(); + + cout << s2pctl_display.DisplayStatisticsInfo(result.statistics_info()) << flush; + + return true; +} + +bool S2pCtlCommands::CommandOperationInfo() +{ + SendCommand(); + + cout << s2pctl_display.DisplayOperationInfo(result.operation_info()) << flush; + + return true; +} + +bool S2pCtlCommands::EvaluateParams(string_view image_params, const string& key1, const string& key2) +{ + if (const auto& components = Split(string(image_params), COMPONENT_SEPARATOR, 2); components.size() == 2) { + SetParam(command, key1, components[0]); + SetParam(command, key2, components[1]); + + return true; + } + + return false; +} + +void S2pCtlCommands::ExportAsBinary(const PbCommand &command, const string &filename) const +{ + const string binary = command.SerializeAsString(); + + ofstream out; + out.open(filename, ios::binary); + out << binary; + if (out.fail()) { + throw io_exception("Error: Can't create protobuf binary file '" + filename + "'"); + } +} + +void S2pCtlCommands::ExportAsJson(const PbCommand &command, const string &filename) const +{ + string json; + MessageToJsonString(command, &json); + + ofstream out(filename); + out << json; + if (out.fail()) { + throw io_exception("Can't create protobuf JSON file '" + filename + "'"); + } +} + +void S2pCtlCommands::ExportAsText(const PbCommand &command, const string &filename) const +{ + string text; + TextFormat::PrintToString(command, &text); + + ofstream out(filename); + out << text; + if (out.fail()) { + throw io_exception("Can't create protobuf text format file '" + filename + "'"); + } +} diff --git a/cpp/s2pctl/s2pctl_commands.h b/cpp/s2pctl/s2pctl_commands.h new file mode 100644 index 00000000..d6a47310 --- /dev/null +++ b/cpp/s2pctl/s2pctl_commands.h @@ -0,0 +1,70 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "generated/command_interface.pb.h" +#include "s2pctl_display.h" +#include + +using namespace command_interface; + +struct sockaddr_in; + +class S2pCtlCommands +{ +public: + + S2pCtlCommands(PbCommand& command, const string& hostname, int port, const string& b, const string& j, const string& t) + : command(command), hostname(hostname), port(port), filename_binary(b), filename_json(j), filename_text(t) {} + ~S2pCtlCommands() = default; + + bool Execute(string_view, string_view, string_view, string_view, string_view); + + bool CommandDevicesInfo(); + +private: + + bool CommandLogLevel(string_view); + bool CommandReserveIds(string_view); + bool CommandCreateImage(string_view); + bool CommandDeleteImage(string_view); + bool CommandRenameImage(string_view); + bool CommandCopyImage(string_view); + bool CommandDefaultImageFolder(string_view); + bool CommandDeviceInfo(); + bool CommandDeviceTypesInfo(); + bool CommandVersionInfo(); + bool CommandServerInfo(); + bool CommandDefaultImageFilesInfo(); + bool CommandImageFileInfo(string_view); + bool CommandNetworkInterfacesInfo(); + bool CommandLogLevelInfo(); + bool CommandReservedIdsInfo(); + bool CommandMappingInfo(); + bool CommandStatisticsInfo(); + bool CommandOperationInfo(); + bool SendCommand(); + bool EvaluateParams(string_view, const string&, const string&); + + void ExportAsBinary(const PbCommand&, const string&) const; + void ExportAsJson(const PbCommand&, const string&) const; + void ExportAsText(const PbCommand&, const string&) const; + + PbCommand& command; + string hostname; + int port; + + string filename_binary; + string filename_json; + string filename_text; + + PbResult result; + + [[no_unique_address]] const S2pCtlDisplay s2pctl_display; +}; diff --git a/cpp/s2pctl/s2pctl_core.h b/cpp/s2pctl/s2pctl_core.h new file mode 100644 index 00000000..b7a8378e --- /dev/null +++ b/cpp/s2pctl/s2pctl_core.h @@ -0,0 +1,28 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include +#include + +using namespace std; + +class ScsiCtl +{ + public: + + ScsiCtl() = default; + ~ScsiCtl() = default; + + int run(const vector&) const; + +private: + + void Banner(const vector&) const; +}; diff --git a/cpp/s2pctl/s2pctl_display.cpp b/cpp/s2pctl/s2pctl_display.cpp new file mode 100644 index 00000000..f4ec061e --- /dev/null +++ b/cpp/s2pctl/s2pctl_display.cpp @@ -0,0 +1,425 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/s2p_util.h" +#include "shared/protobuf_util.h" +#include "s2pctl_display.h" +#include +#include +#include +#include +#include + +using namespace std; +using namespace command_interface; +using namespace s2p_util; +using namespace protobuf_util; + +string S2pCtlDisplay::DisplayDevicesInfo(const PbDevicesInfo& devices_info) const +{ + ostringstream s; + + const vector devices(devices_info.devices().begin(), devices_info.devices().end()); + + s << ListDevices(devices); + + return s.str(); +} + +string S2pCtlDisplay::DisplayDeviceInfo(const PbDevice& pb_device) const +{ + ostringstream s; + + s << " " << pb_device.id() << ":" << pb_device.unit() << " " << PbDeviceType_Name(pb_device.type()) + << " " << pb_device.vendor() << ":" << pb_device.product() << ":" << pb_device.revision(); + + if (pb_device.block_size()) { + s << " " << pb_device.block_size() << " bytes per sector"; + + if (pb_device.block_count()) { + s << " " << pb_device.block_size() * pb_device.block_count() << " bytes capacity"; + } + } + + if (pb_device.properties().supports_file() && !pb_device.file().name().empty()) { + s << " " << pb_device.file().name(); + } + + s << " "; + + vector properties; + + if (pb_device.properties().read_only()) { + properties.emplace_back("read-only"); + } + + if (pb_device.properties().protectable() && pb_device.status().protected_()) { + properties.emplace_back("protected"); + } + + if (pb_device.properties().stoppable() && pb_device.status().stopped()) { + properties.emplace_back("stopped"); + } + + if (pb_device.properties().removable() && pb_device.status().removed()) { + properties.emplace_back("removed"); + } + + if (pb_device.properties().lockable() && pb_device.status().locked()) { + properties.emplace_back("locked"); + } + + if (!properties.empty()) { + s << Join(properties) << " "; + } + + s << DisplayParams(pb_device) << '\n'; + + return s.str(); +} + +string S2pCtlDisplay::DisplayVersionInfo(const PbVersionInfo& version_info) const +{ + ostringstream s; + + s << "Server version: "; + if (version_info.identifier().empty() || version_info.major_version() >= 21) { + if (version_info.major_version() == 21 && version_info.minor_version() < 12) { + s << "RaSCSI"; + } + else { + s << "PiSCSI"; + } + s << setw(2); + } + else { + s << version_info.identifier(); + } + + s << " " << version_info.major_version() << "."; + + s << version_info.minor_version(); + + if (version_info.patch_version() > 0) { + s << "." << version_info.patch_version(); + } + else if (version_info.patch_version() < 0) { + s << " (development version)"; + } + + s << '\n'; + + return s.str(); +} + +string S2pCtlDisplay::DisplayLogLevelInfo(const PbLogLevelInfo& log_level_info) const +{ + ostringstream s; + + if (!log_level_info.log_levels_size()) { + s << " No log level settings available\n"; + } + else { + s << "s2p log levels, sorted by severity:\n"; + + for (const auto& log_level : log_level_info.log_levels()) { + s << " " << log_level << '\n'; + } + } + + s << "Current s2p log level: " << log_level_info.current_log_level() << '\n'; + + return s.str(); +} + +string S2pCtlDisplay::DisplayDeviceTypesInfo(const PbDeviceTypesInfo& device_types_info) const +{ + ostringstream s; + + s << "Supported device types and their properties:"; + + for (const auto& device_type_info : device_types_info.properties()) { + s << "\n " << PbDeviceType_Name(device_type_info.type()) << " "; + + const PbDeviceProperties& properties = device_type_info.properties(); + + s << DisplayAttributes(properties); + + if (properties.supports_file()) { + s << "Image file support\n "; + } + + if (properties.supports_params()) { + s << "Parameter support\n "; + } + + s << DisplayDefaultParameters(properties); + + s << DisplayBlockSizes(properties); + } + + s << '\n'; + + return s.str(); +} + +string S2pCtlDisplay::DisplayReservedIdsInfo(const PbReservedIdsInfo& reserved_ids_info) const +{ + ostringstream s; + + if (reserved_ids_info.ids_size()) { + const set sorted_ids(reserved_ids_info.ids().begin(), reserved_ids_info.ids().end()); + s << "Reserved device IDs: " << Join(sorted_ids) << '\n'; + } + + return s.str(); +} + +string S2pCtlDisplay::DisplayImageFile(const PbImageFile& image_file_info) const +{ + ostringstream s; + + s << image_file_info.name() << " " << image_file_info.size() << " bytes"; + + if (image_file_info.read_only()) { + s << " read-only"; + } + + if (image_file_info.type() != UNDEFINED) { + s << " " << PbDeviceType_Name(image_file_info.type()); + } + + s << '\n'; + + return s.str(); +} + +string S2pCtlDisplay::DisplayImageFilesInfo(const PbImageFilesInfo& image_files_info) const +{ + ostringstream s; + + s << "Default image file folder: " << image_files_info.default_image_folder() << '\n'; + s << "Supported folder depth: " << image_files_info.depth() << '\n'; + + if (!image_files_info.image_files().empty()) { + vector image_files(image_files_info.image_files().begin(), image_files_info.image_files().end()); + ranges::sort(image_files, [](const auto& a, const auto& b) { return a.name() < b.name(); }); + + s << "Available image files:\n"; + for (const auto& image_file : image_files) { + s << " "; + + s << DisplayImageFile(image_file); + } + } + + return s.str(); +} + +string S2pCtlDisplay::DisplayNetworkInterfaces(const PbNetworkInterfacesInfo& network_interfaces_info) const +{ + ostringstream s; + + const set> sorted_interfaces(network_interfaces_info.name().begin(), network_interfaces_info.name().end()); + s << "Available (up) network interfaces: " << Join(sorted_interfaces) << '\n'; + + return s.str(); +} + +string S2pCtlDisplay::DisplayMappingInfo(const PbMappingInfo& mapping_info) const +{ + ostringstream s; + + s << "Supported image file extension to device type mappings:\n"; + + for (const map> sorted_mappings(mapping_info.mapping().begin(), mapping_info.mapping().end()); + const auto& [extension, type] : sorted_mappings) { + s << " " << extension << "->" << PbDeviceType_Name(type) << '\n'; + } + + return s.str(); +} + +string S2pCtlDisplay::DisplayStatisticsInfo(const PbStatisticsInfo& statistics_info) const +{ + ostringstream s; + + s << "Statistics:\n"; + + // Sort by ascending ID, LUN and key and by descending category + vector sorted_statistics = { statistics_info.statistics().begin(), statistics_info.statistics().end() }; + ranges::sort(sorted_statistics, [] (const PbStatistics& a, const PbStatistics& b) { + if (a.category() > b.category()) return true; + if (a.category() < b.category()) return false; + if (a.id() < b.id()) return true; + if (a.id() > b.id()) return false; + if (a.unit() < b.unit()) return true; + if (a.unit() > b.unit()) return false; + return a.key() < b.key(); + }); + + PbStatisticsCategory prev_category = PbStatisticsCategory::CATEGORY_NONE; + for (const auto& statistics : sorted_statistics) { + if (statistics.category() != prev_category) { + // Strip leading "CATEGORY_" + s << " " << PbStatisticsCategory_Name(statistics.category()).substr(9) << '\n'; + prev_category = statistics.category(); + } + + s << " " << statistics.id() << ":" << statistics.unit() << " " << statistics.key() << ": " << statistics.value() << '\n'; + } + + return s.str(); +} + +string S2pCtlDisplay::DisplayOperationInfo(const PbOperationInfo& operation_info) const +{ + ostringstream s; + + const map> operations(operation_info.operations().begin(), operation_info.operations().end()); + + // Copies result into a map sorted by operation name + auto unknown_operation = make_unique(); + map> sorted_operations; + + for (const auto& [ordinal, meta_data] : operations) { + if (PbOperation_IsValid(static_cast(ordinal))) { + sorted_operations[PbOperation_Name(static_cast(ordinal))] = meta_data; + } + else { + // If the server-side operation is unknown for the client use the server-provided operation name + // No further operation information is available in this case + sorted_operations[meta_data.server_side_name()] = *unknown_operation; + } + } + + s << "Operations supported by s2p server and their parameters:\n"; + for (const auto& [name, meta_data] : sorted_operations) { + if (!meta_data.server_side_name().empty()) { + s << " " << name; + if (!meta_data.description().empty()) { + s << " (" << meta_data.description() << ")"; + } + s << '\n'; + + s << DisplayParameters(meta_data); + } + else { + s << " " << name << " (Unknown server-side operation)\n"; + } + } + + return s.str(); +} + +string S2pCtlDisplay::DisplayParams(const PbDevice& pb_device) const +{ + ostringstream s; + + set> params; + for (const auto& [key, value] : pb_device.params()) { + params.insert(key + "=" + value); + } + + s << Join(params, ":"); + + return s.str(); +} + +string S2pCtlDisplay::DisplayAttributes(const PbDeviceProperties& props) const +{ + ostringstream s; + + vector properties; + if (props.read_only()) { + properties.emplace_back("read-only"); + } + if (props.protectable()) { + properties.emplace_back("protectable"); + } + if (props.stoppable()) { + properties.emplace_back("stoppable"); + } + if (props.removable()) { + properties.emplace_back("removable"); + } + if (props.lockable()) { + properties.emplace_back("lockable"); + } + + if (!properties.empty()) { + s << "Properties: " << Join(properties) << "\n "; + } + + return s.str(); +} + +string S2pCtlDisplay::DisplayDefaultParameters(const PbDeviceProperties& properties) const +{ + ostringstream s; + + if (!properties.default_params().empty()) { + set> params; + for (const auto& [key, value] : properties.default_params()) { + params.insert(key + "=" + value); + } + + s << "Default parameters: " << Join(params, "\n "); + } + + return s.str(); +} + +string S2pCtlDisplay::DisplayBlockSizes(const PbDeviceProperties& properties) const +{ + ostringstream s; + + if (properties.block_sizes_size()) { + const set sorted_sizes(properties.block_sizes().begin(), properties.block_sizes().end()); + s << "Configurable block sizes in bytes: " << Join(sorted_sizes); + } + + return s.str(); +} + +string S2pCtlDisplay::DisplayParameters(const PbOperationMetaData& meta_data) const +{ + vector sorted_parameters(meta_data.parameters().begin(), meta_data.parameters().end()); + ranges::sort(sorted_parameters, [](const auto& a, const auto& b) { return a.name() < b.name(); }); + + ostringstream s; + + for (const auto& parameter : sorted_parameters) { + s << " " << parameter.name() << ": " + << (parameter.is_mandatory() ? "mandatory" : "optional"); + + if (!parameter.description().empty()) { + s << " (" << parameter.description() << ")"; + } + s << '\n'; + + s << DisplayPermittedValues(parameter); + + if (!parameter.default_value().empty()) { + s << " Default value: " << parameter.default_value() << '\n'; + } + } + + return s.str(); +} + +string S2pCtlDisplay::DisplayPermittedValues(const PbOperationParameter& parameter) const +{ + ostringstream s; + if (parameter.permitted_values_size()) { + const set> sorted_values(parameter.permitted_values().begin(), parameter.permitted_values().end()); + s << " Permitted values: " << Join(sorted_values) << '\n'; + } + + return s.str(); +} diff --git a/cpp/s2pctl/s2pctl_display.h b/cpp/s2pctl/s2pctl_display.h new file mode 100644 index 00000000..ac07817f --- /dev/null +++ b/cpp/s2pctl/s2pctl_display.h @@ -0,0 +1,45 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "generated/command_interface.pb.h" +#include + +using namespace std; +using namespace command_interface; + +class S2pCtlDisplay +{ +public: + + S2pCtlDisplay() = default; + ~S2pCtlDisplay() = default; + + string DisplayDevicesInfo(const PbDevicesInfo&) const; + string DisplayDeviceInfo(const PbDevice&) const; + string DisplayVersionInfo(const PbVersionInfo&) const; + string DisplayLogLevelInfo(const PbLogLevelInfo&) const; + string DisplayDeviceTypesInfo(const PbDeviceTypesInfo&) const; + string DisplayReservedIdsInfo(const PbReservedIdsInfo&) const; + string DisplayImageFile(const PbImageFile&) const; + string DisplayImageFilesInfo(const PbImageFilesInfo&) const; + string DisplayNetworkInterfaces(const PbNetworkInterfacesInfo&) const; + string DisplayMappingInfo(const PbMappingInfo&) const; + string DisplayStatisticsInfo(const PbStatisticsInfo&) const; + string DisplayOperationInfo(const PbOperationInfo&) const; + +private: + + string DisplayParams(const PbDevice&) const; + string DisplayAttributes(const PbDeviceProperties&) const; + string DisplayDefaultParameters(const PbDeviceProperties&) const; + string DisplayBlockSizes(const PbDeviceProperties&) const; + string DisplayParameters(const PbOperationMetaData&) const; + string DisplayPermittedValues(const PbOperationParameter&) const; +}; diff --git a/cpp/s2pctl/s2pctl_parser.cpp b/cpp/s2pctl/s2pctl_parser.cpp new file mode 100644 index 00000000..829c2918 --- /dev/null +++ b/cpp/s2pctl/s2pctl_parser.cpp @@ -0,0 +1,29 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "s2pctl_parser.h" + +PbOperation S2pCtlParser::ParseOperation(string_view operation) const +{ + const auto& it = operations.find(tolower(operation[0])); + return it != operations.end() ? it->second : NO_OPERATION; +} + +PbDeviceType S2pCtlParser::ParseType(const string& type) const +{ + string t; + ranges::transform(type, back_inserter(t), ::toupper); + + if (PbDeviceType parsed_type; PbDeviceType_Parse(t, &parsed_type)) { + return parsed_type; + } + + // Handle convenience device types (shortcuts) + const auto& it = device_types.find(tolower(type[0])); + return it != device_types.end() ? it->second : UNDEFINED; +} diff --git a/cpp/s2pctl/s2pctl_parser.h b/cpp/s2pctl/s2pctl_parser.h new file mode 100644 index 00000000..4181d4ce --- /dev/null +++ b/cpp/s2pctl/s2pctl_parser.h @@ -0,0 +1,48 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include +#include +#include "generated/command_interface.pb.h" + +using namespace std; +using namespace command_interface; + +class S2pCtlParser +{ + +public: + + S2pCtlParser() = default; + ~S2pCtlParser() = default; + + PbOperation ParseOperation(string_view) const; + PbDeviceType ParseType(const string&) const; + +private: + + const unordered_map operations = { + { 'a', ATTACH }, + { 'd', DETACH }, + { 'e', EJECT }, + { 'i', INSERT }, + { 'p', PROTECT }, + { 's', DEVICES_INFO }, + { 'u', UNPROTECT } + }; + + const unordered_map device_types = { + { 'c', SCCD }, + { 'd', SCDP }, + { 'h', SCHD }, + { 'l', SCLP }, + { 'm', SCMO }, + { 'r', SCRM }, + { 's', SCHS } + }; +}; diff --git a/cpp/s2pctl/sp2ctl_core.cpp b/cpp/s2pctl/sp2ctl_core.cpp new file mode 100644 index 00000000..1630cf33 --- /dev/null +++ b/cpp/s2pctl/sp2ctl_core.cpp @@ -0,0 +1,333 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Powered by XM6 TypeG Technology. +// Copyright (C) 2016-2020 GIMONS +// Copyright (C) 2020-2023 Contributors to the PiSCSI project +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "controllers/controller_factory.h" +#include "controllers/scsi_controller.h" +#include "shared/s2p_util.h" +#include "shared/protobuf_util.h" +#include "shared/shared_exceptions.h" +#include "shared/s2p_version.h" +#include "s2pctl/s2pctl_parser.h" +#include "s2pctl/s2pctl_commands.h" +#include "s2pctl/s2pctl_core.h" +#include +#include +#include +#include + +using namespace std; +using namespace command_interface; +using namespace s2p_util; +using namespace protobuf_util; + +void ScsiCtl::Banner(const vector& args) const +{ + if (args.size() < 2) { + cout << s2p_util::Banner("(Controller App)") + << "\nUsage: " << args[0] << " -i ID[:LUN] [-c CMD] [-C FILE] [-t TYPE] [-b BLOCK_SIZE] [-n NAME] [-f FILE|PARAM] " + << "[-F IMAGE_FOLDER] [-L LOG_LEVEL] [-h HOST] [-p PORT] [-r RESERVED_IDS] " + << "[-C FILENAME:FILESIZE] [-d FILENAME] [-B FILENAME] [-J FILENAME] [-T FILENAME] [-R CURRENT_NAME:NEW_NAME] " + << "[-x CURRENT_NAME:NEW_NAME] [-z LOCALE] " + << "[-e] [-E FILENAME] [-D] [-I] [-l] [-m] [o] [-O] [-P] [-s] [-S] [-v] [-V] [-y] [-X]\n" + << " where ID[:LUN] ID := {0-" << (ControllerFactory::GetIdMax() - 1) << "}," + << " SCSI LUN := {0-" << (ControllerFactory::GetScsiLunMax() - 1) << "}, default is 0\n" + << " SASI LUN := {0-" << (ControllerFactory::GetSasiLunMax() - 1) << "}, default is 0\n" + << " CMD := {attach|detach|insert|eject|protect|unprotect|show}\n" + << " TYPE := {schd|scrm|sccd|scmo|scbr|scdp} or convenience type {hd|rm|mo|cd|bridge|daynaport}\n" + << " BLOCK_SIZE := {512|1024|2048|4096) bytes per hard disk drive block\n" + << " NAME := name of device to attach (VENDOR:PRODUCT:REVISION)\n" + << " FILE|PARAM := image file path or device-specific parameter\n" + << " IMAGE_FOLDER := default location for image files, default is '~/images'\n" + << " HOST := s2p host to connect to, default is 'localhost'\n" + << " PORT := s2p port to connect to, default is 6868\n" + << " RESERVED_IDS := comma-separated list of IDs to reserve\n" + << " LOG_LEVEL := log level {trace|debug|info|warn|err|off}, default is 'info'\n" + << " If CMD is 'attach' or 'insert' the FILE parameter is required.\n" + << "Usage: " << args[0] << " -l\n" + << " Print device list.\n" << flush; + + exit(EXIT_SUCCESS); + } +} + +int ScsiCtl::run(const vector& args) const +{ + GOOGLE_PROTOBUF_VERIFY_VERSION; + + Banner(args); + + S2pCtlParser parser; + PbCommand command; + PbDeviceDefinition* device = command.add_devices(); + device->set_id(-1); + string hostname = "localhost"; + int port = 6868; + string param; + string log_level; + string default_folder; + string reserved_ids; + string image_params; + string filename; + string filename_json; + string filename_binary; + string filename_text; + string token; + bool list = false; + + string locale = GetLocale(); + + optind = 1; + opterr = 0; + int opt; + while ((opt = getopt(static_cast(args.size()), args.data(), + "e::lmos::vDINOSTVXa:b:c:d:f:h:i:n:p:r:t:x:z:B:C:E:F:J:L:P::R:Z:")) != -1) { + switch (opt) { + case 'i': + if (const string error = SetIdAndLun(ControllerFactory::GetIdMax(), ControllerFactory::GetLunMax(), + *device, optarg); !error.empty()) { + cerr << "Error: " << error << endl; + exit(EXIT_FAILURE); + } + break; + + case 'C': + command.set_operation(CREATE_IMAGE); + image_params = optarg; + break; + + case 'b': + int block_size; + if (!GetAsUnsignedInt(optarg, block_size)) { + cerr << "Error: Invalid block size " << optarg << endl; + exit(EXIT_FAILURE); + } + device->set_block_size(block_size); + break; + + case 'c': + command.set_operation(parser.ParseOperation(optarg)); + if (command.operation() == NO_OPERATION) { + cerr << "Error: Unknown operation '" << optarg << "'" << endl; + exit(EXIT_FAILURE); + } + break; + + case 'D': + command.set_operation(DETACH_ALL); + break; + + case 'd': + command.set_operation(DELETE_IMAGE); + image_params = optarg; + break; + + case 'E': + filename = optarg; + if (filename.empty()) { + cerr << "Error: Missing filename" << endl; + exit(EXIT_FAILURE); + } + command.set_operation(IMAGE_FILE_INFO); + break; + + case 'e': + command.set_operation(DEFAULT_IMAGE_FILES_INFO); + if (optarg) { + SetCommandParams(command, optarg); + } + break; + + case 'F': + command.set_operation(DEFAULT_FOLDER); + default_folder = optarg; + break; + + case 'f': + param = optarg; + break; + + case 'h': + hostname = optarg; + if (hostname.empty()) { + cerr << "Error: Missing hostname" << endl; + exit(EXIT_FAILURE); + } + break; + + case 'B': + filename_binary = optarg; + if (filename_binary.empty()) { + cerr << "Error: Missing filename" << endl; + exit(EXIT_FAILURE); + } + break; + + case 'J': + filename_json = optarg; + if (filename_json.empty()) { + cerr << "Error: Missing filename" << endl; + exit(EXIT_FAILURE); + } + break; + + case 'Z': + filename_text = optarg; + if (filename_text.empty()) { + cerr << "Error: Missing filename" << endl; + exit(EXIT_FAILURE); + } + break; + + case 'I': + command.set_operation(RESERVED_IDS_INFO); + break; + + case 'L': + command.set_operation(LOG_LEVEL); + log_level = optarg; + break; + + case 'l': + list = true; + break; + + case 'm': + command.set_operation(MAPPING_INFO); + break; + + case 'N': + command.set_operation(NETWORK_INTERFACES_INFO); + break; + + case 'O': + command.set_operation(LOG_LEVEL_INFO); + break; + + case 'o': + command.set_operation(OPERATION_INFO); + break; + + case 't': + device->set_type(parser.ParseType(optarg)); + if (device->type() == UNDEFINED) { + cerr << "Error: Unknown device type '" << optarg << "'" << endl; + exit(EXIT_FAILURE); + } + break; + + case 'r': + command.set_operation(RESERVE_IDS); + reserved_ids = optarg; + break; + + case 'R': + command.set_operation(RENAME_IMAGE); + image_params = optarg; + break; + + case 'n': + SetProductData(*device, optarg); + break; + + case 'p': + if (!GetAsUnsignedInt(optarg, port) || port <= 0 || port > 65535) { + cerr << "Error: Invalid port " << optarg << ", port must be between 1 and 65535" << endl; + exit(EXIT_FAILURE); + } + break; + + case 's': + command.set_operation(SERVER_INFO); + if (optarg) { + if (const string error = SetCommandParams(command, optarg); !error.empty()) { + cerr << "Error: " << error << endl; + exit(EXIT_FAILURE); + } + } + break; + + case 'S': + command.set_operation(STATISTICS_INFO); + break; + + case 'v': + cout << "s2pctl version: " << GetVersionString() << '\n'; + exit(EXIT_SUCCESS); + break; + + case 'P': + token = optarg ? optarg : getpass("Password: "); + break; + + case 'V': + command.set_operation(VERSION_INFO); + break; + + case 'x': + command.set_operation(COPY_IMAGE); + image_params = optarg; + break; + + case 'T': + command.set_operation(DEVICE_TYPES_INFO); + break; + + case 'X': + command.set_operation(SHUT_DOWN); + SetParam(command, "mode", "rascsi"); + break; + + case 'z': + locale = optarg; + break; + + default: + break; + } + } + + // BSD getopt stops after the first free argument. Thie work-around below cannot really address this. +#ifdef __linux__ + if (optopt) { + exit(EXIT_FAILURE); + } +#endif + + SetParam(command, "token", token); + SetParam(command, "locale", locale); + + S2pCtlCommands s2pctl_commands(command, hostname, port, filename_binary, filename_json, filename_text); + + bool status; + try { + // Listing devices is a special case (legacy rasctl backwards compatibility) + if (list) { + command.clear_devices(); + command.set_operation(DEVICES_INFO); + + status = s2pctl_commands.CommandDevicesInfo(); + } + else { + ParseParameters(*device, param); + + status = s2pctl_commands.Execute(log_level, default_folder, reserved_ids, image_params, filename); + } + } + catch(const io_exception& e) { + cerr << "Error: " << e.what() << endl; + + status = false; + + // Fall through + } + + return status ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/cpp/s2pdump/s2pdump.cpp b/cpp/s2pdump/s2pdump.cpp new file mode 100644 index 00000000..989dfc65 --- /dev/null +++ b/cpp/s2pdump/s2pdump.cpp @@ -0,0 +1,18 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "s2pdump/s2pdump_core.h" + +using namespace std; + +int main(int argc, char *argv[]) +{ + vector args(argv, argv + argc); + + return S2pDump().run(args); +} diff --git a/cpp/s2pdump/s2pdump_core.cpp b/cpp/s2pdump/s2pdump_core.cpp new file mode 100644 index 00000000..d8f95028 --- /dev/null +++ b/cpp/s2pdump/s2pdump_core.cpp @@ -0,0 +1,558 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022 akuker +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "s2pdump/s2pdump_core.h" +#include "buses/bus_factory.h" +#include "controllers/controller_factory.h" +#include "shared/shared_exceptions.h" +#include "shared/s2p_util.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace filesystem; +using namespace spdlog; +using namespace scsi_defs; +using namespace s2p_util; + +void S2pDump::CleanUp() const +{ + if (bus) { + bus->CleanUp(); + } +} + +void S2pDump::TerminationHandler(int) +{ + instance->bus->SetRST(true); + + instance->CleanUp(); + + // Process will terminate automatically +} + +bool S2pDump::Banner(span args) const +{ + if (args.size() > 1 && (string(args[1]) == "-h" || string(args[1]) == "--help")) { + cout << s2p_util::Banner("(Hard Disk Dump/Restore Utility)") + << "Usage: " << args[0] << " -t ID[:LUN] [-i BID] [-f FILE] [-a] [-r] [-b BUFFER_SIZE]" + << " [-L log_level] [-p] [-I] [-s]\n" + << " ID is the target device ID (0-" << (ControllerFactory::GetIdMax() - 1) << ").\n" + << " LUN is the optional target device LUN (0-" << (ControllerFactory::GetScsiLunMax() - 1) << ")." + << " Default is 0.\n" + << " BID is the board ID (0-7). Default is 7.\n" + << " FILE is the image file path. Only needed when not dumping to stdout and no property file is" + << " requested.\n" + << " BUFFER_SIZE is the transfer buffer size in bytes, at least " << MINIMUM_BUFFER_SIZE + << " bytes. Default is 1 MiB.\n" + << " -a Scan all potential LUNs during bus scan, default is LUN 0 only.\n" + << " -r Restore instead of dump.\n" + << " -p Generate .properties file to be used with the web interface." + << " Only valid for dump and inquiry mode.\n" + << " -I Display INQUIRY data of ID[:LUN].\n" + << " -s Scan SCSI bus for devices.\n" + << flush; + + return false; + } + + return true; +} + +bool S2pDump::Init(bool in_process) +{ + instance = this; + // Signal handler for cleaning up + struct sigaction termination_handler; + termination_handler.sa_handler = TerminationHandler; + sigemptyset(&termination_handler.sa_mask); + termination_handler.sa_flags = 0; + sigaction(SIGINT, &termination_handler, nullptr); + sigaction(SIGTERM, &termination_handler, nullptr); + signal(SIGPIPE, SIG_IGN); + + bus_factory = make_unique(); + + bus = bus_factory->CreateBus(BUS::mode_e::INITIATOR, in_process); + if (bus != nullptr) { + scsi_executor = make_unique(*bus, initiator_id); + } + + return bus != nullptr; +} + +void S2pDump::ParseArguments(span args) +{ + int buffer_size = DEFAULT_BUFFER_SIZE; + + optind = 1; + opterr = 0; + int opt; + while ((opt = getopt(static_cast(args.size()), args.data(), "i:f:b:t:L:arspI")) != -1) { + switch (opt) { + case 'i': + if (!GetAsUnsignedInt(optarg, initiator_id) || initiator_id > 7) { + throw parser_exception("Invalid board ID " + to_string(initiator_id) + " (0-7)"); + } + break; + + case 'f': + filename = optarg; + break; + + case 'I': + run_inquiry = true; + break; + + case 'b': + if (!GetAsUnsignedInt(optarg, buffer_size) || buffer_size < MINIMUM_BUFFER_SIZE) { + throw parser_exception( + "Buffer size must be at least " + to_string(MINIMUM_BUFFER_SIZE / 1024) + " KiB"); + } + + break; + + case 's': + run_bus_scan = true; + break; + + case 't': + if (const string error = ProcessId(ControllerFactory::GetIdMax(), ControllerFactory::GetLunMax(), + optarg, target_id, target_lun); !error.empty()) { + throw parser_exception(error); + } + break; + + case 'L': + log_level = optarg; + break; + + case 'a': + scan_all_luns = true; + break; + + case 'r': + restore = true; + break; + + case 'p': + create_properties_file = true; + break; + + default: + break; + } + } + + if (target_lun == -1) { + target_lun = 0; + } + + if (run_bus_scan) { + run_inquiry = false; + } + + buffer = vector(buffer_size); +} + +int S2pDump::run(span args, bool in_process) +{ + to_stdout = !isatty(STDOUT_FILENO); + + // Prevent any logging when dumping to stdout + if (to_stdout) { + spdlog::set_level(level::off); + } + + if (!Banner(args)) { + return EXIT_SUCCESS; + } + + try { + ParseArguments(args); + } + catch (const parser_exception &e) { + cerr << "Error: " << e.what() << endl; + return EXIT_FAILURE; + } + + if (!run_bus_scan && target_id == -1) { + cerr << "Missing target ID" << endl; + return EXIT_FAILURE; + } + + if (target_id == initiator_id) { + cerr << "Target ID and board ID must not be identical" << endl; + return EXIT_FAILURE; + } + + if ((filename.empty() && !run_bus_scan && !run_inquiry && !to_stdout) || create_properties_file) { + cerr << "Missing filename" << endl; + return EXIT_FAILURE; + } + + if (!in_process && getuid()) { + cerr << "Error: GPIO bus access requires root permissions" << endl; + return EXIT_FAILURE; + } + + if (!Init(in_process)) { + cerr << "Error: Can't initialize bus" << endl; + return EXIT_FAILURE; + } + + if (!in_process && !bus_factory->IsRaspberryPi()) { + cerr << "Error: There is no board hardware support" << endl; + return EXIT_FAILURE; + } + + if (!to_stdout && !SetLogLevel()) { + cerr << "Error: Invalid log level '" + log_level + "'"; + return EXIT_FAILURE; + } + + if (run_bus_scan) { + ScanBus(); + } + else if (run_inquiry) { + DisplayBoardId(); + + if (DisplayInquiry(false) && create_properties_file && !filename.empty()) { + inq_info.GeneratePropertiesFile(filename + ".properties"); + } + } + else { + if (const string error = DumpRestore(); !error.empty()) { + cerr << "Error: " << error << endl; + CleanUp(); + return EXIT_FAILURE; + } + } + + CleanUp(); + + return EXIT_SUCCESS; +} + +void S2pDump::DisplayBoardId() const +{ + if (!to_stdout) { + cout << DIVIDER << "\nBoard ID is " << initiator_id << "\n"; + } +} + +void S2pDump::ScanBus() +{ + DisplayBoardId(); + + for (target_id = 0; target_id < ControllerFactory::GetIdMax(); target_id++) { + if (initiator_id == target_id) { + continue; + } + + target_lun = 0; + if (!DisplayInquiry(false) || !scan_all_luns) { + // Continue with next ID if there is no LUN 0 or only LUN 0 should be scanned + continue; + } + + auto luns = scsi_executor->ReportLuns(); + // LUN 0 has already been dealt with + luns.erase(0); + + for (const auto lun : luns) { + target_lun = lun; + DisplayInquiry(false); + } + } +} + +bool S2pDump::DisplayInquiry(bool check_type) +{ + cout << DIVIDER << "\nScanning target ID:LUN " << target_id << ":" << target_lun << "\n" << flush; + + inq_info = { }; + + scsi_executor->SetTarget(target_id, target_lun); + + vector buf(36); + + if (!scsi_executor->Inquiry(buf)) { + return false; + } + + const auto type = static_cast(buf[0]); + if ((type & byte { 0x1f }) == byte { 0x1f }) { + // Requested LUN is not available + return false; + } + + array str = { }; + memcpy(str.data(), &buf[8], 8); + inq_info.vendor = string(str.data()); + cout << "Vendor: " << inq_info.vendor << "\n"; + + str.fill(0); + memcpy(str.data(), &buf[16], 16); + inq_info.product = string(str.data()); + cout << "Product: " << inq_info.product << "\n"; + + str.fill(0); + memcpy(str.data(), &buf[32], 4); + inq_info.revision = string(str.data()); + cout << "Revision: " << inq_info.revision << "\n" << flush; + + if (const auto &t = DEVICE_TYPES.find(type & byte { 0x1f }); t != DEVICE_TYPES.end()) { + cout << "Device Type: " << (*t).second << "\n"; + } + else { + cout << "Device Type: Unknown\n"; + } + + cout << "Removable: " << (((static_cast(buf[1]) & byte { 0x80 }) == byte { 0x80 }) ? "Yes" : "No") + << "\n"; + + if (check_type && type != static_cast(device_type::direct_access) && + type != static_cast(device_type::cd_rom) && type != static_cast(device_type::optical_memory)) { + cerr << "Error: Invalid device type, supported types for dump/restore are DIRECT ACCESS," + << " CD-ROM/DVD/BD and OPTICAL MEMORY" << endl; + return false; + } + + return true; +} + +string S2pDump::DumpRestore() +{ + if (!GetDeviceInfo()) { + return "Can't get device information"; + } + + fstream fs; + if (!to_stdout) { + fs.open(filename, (restore ? ios::in : ios::out) | ios::binary); + if (fs.fail()) { + return "Can't open image file '" + filename + "': " + strerror(errno); + } + } + + ostream &out = to_stdout ? cout : fs; + + const auto effective_size = CalculateEffectiveSize(); + if (effective_size < 0) { + return ""; + } + if (!effective_size) { + cerr << "Nothing to do, effective size is 0\n" << flush; + return ""; + } + + if (!to_stdout) { + cout << "Starting " << (restore ? "restore" : "dump") << ", buffer size is " << buffer.size() + << " bytes\n\n" << flush; + } + + int sector_offset = 0; + + auto remaining = effective_size; + + scsi_executor->SetTarget(target_id, target_lun); + + const auto start_time = chrono::high_resolution_clock::now(); + + while (remaining) { + const auto byte_count = static_cast(min(static_cast(remaining), buffer.size())); + auto sector_count = byte_count / inq_info.sector_size; + if (byte_count % inq_info.sector_size) { + ++sector_count; + } + + spdlog::debug("Remaining bytes: " + to_string(remaining)); + spdlog::debug("Next sector: " + to_string(sector_offset)); + spdlog::debug("Sector count: " + to_string(sector_count)); + spdlog::debug("SCSI transfer size: " + to_string(sector_count * inq_info.sector_size)); + spdlog::debug("File chunk size: " + to_string(byte_count)); + + if (const string error = ReadWrite(out, fs, sector_offset, sector_count, byte_count); !error.empty()) { + return error; + } + + sector_offset += sector_count; + remaining -= byte_count; + + if (!to_stdout) { + cout << setw(3) << (effective_size - remaining) * 100 / effective_size << "% (" << effective_size - remaining + << "/" << effective_size << ")\n" << flush; + } + } + + auto duration = chrono::duration_cast(chrono::high_resolution_clock::now() + - start_time).count(); + if (!duration) { + duration = 1; + } + + if (restore) { + // Ensure that if the target device is also a SCSI2Pi instance its image file becomes complete immediately + scsi_executor->SynchronizeCache(); + } + + if (!to_stdout) { + cout << DIVIDER + << "\nTransferred " << effective_size / 1024 / 1024 << " MiB (" << effective_size << " bytes)" + << "\nTotal time: " << duration << " seconds (" << duration / 60 << " minutes)" + << "\nAverage transfer rate: " << effective_size / duration << " bytes per second (" + << effective_size / 1024 / duration << " KiB per second)\n" + << DIVIDER << "\n" << flush; + } + + if (create_properties_file && !restore) { + inq_info.GeneratePropertiesFile(filename + ".properties"); + } + + return ""; +} + +string S2pDump::ReadWrite(ostream &out, fstream &fs, int sector_offset, uint32_t sector_count, int byte_count) +{ + if (restore) { + fs.read((char*) buffer.data(), byte_count); + if (fs.fail()) { + return "Error reading from file '" + filename + "'"; + } + + if (!scsi_executor->ReadWrite(buffer, sector_offset, sector_count, + sector_count * inq_info.sector_size, true)) { + return "Error writing to device"; + } + } else { + if (!scsi_executor->ReadWrite(buffer, sector_offset, + sector_count, sector_count * inq_info.sector_size, false)) { + return "Error reading from device"; + } + + out.write((const char*) buffer.data(), byte_count); + if (out.fail()) { + return "Error writing to file '" + filename + "'"; + } + } + + return ""; +} + +long S2pDump::CalculateEffectiveSize() const +{ + const off_t disk_size = inq_info.capacity * inq_info.sector_size; + + size_t effective_size; + if (restore) { + off_t size; + try { + size = file_size(path(filename)); + } + catch (const filesystem_error &e) { + cerr << "Can't determine image file size: " << e.what() << endl; + return -1; + } + + effective_size = min(size, disk_size); + + if (!to_stdout) { + cout << "Restore image file size: " << size << " bytes\n" << flush; + } + + if (size > disk_size) { + cerr << "Warning: Image file size of " << size + << " byte(s) is larger than disk size of " << disk_size << " bytes(s)\n" << flush; + } else if (size < disk_size) { + cerr << "Warning: Image file size of " << size + << " byte(s) is smaller than disk size of " << disk_size << " bytes(s)\n" << flush; + } + } else { + effective_size = disk_size; + } + + return static_cast(effective_size); +} + +bool S2pDump::GetDeviceInfo() +{ + DisplayBoardId(); + + if (!DisplayInquiry(true)) { + return false; + } + + // Clear any pending condition, e.g. medium just having being inserted + scsi_executor->TestUnitReady(); + + const auto [capacity, sector_size] = scsi_executor->ReadCapacity(); + if (!capacity || !sector_size) { + spdlog::trace("Can't get device capacity"); + return false; + } + + inq_info.capacity = capacity; + inq_info.sector_size = sector_size; + + if (!to_stdout) { + cout << "Sectors: " << capacity << "\n" + << "Sector size: " << sector_size << " bytes\n" + << "Capacity: " << sector_size * capacity / 1024 / 1024 << " MiB (" << sector_size * capacity + << " bytes)\n" + << DIVIDER << "\n\n" + << flush; + } + + return true; +} + +void S2pDump::inquiry_info::GeneratePropertiesFile(const string &properties_file) const +{ + ofstream prop(properties_file); + + prop << "{\n" + << " \"vendor\": \"" << vendor << "\",\n" + << " \"product\": \"" << product << "\",\n" + << " \"revision\": \"" << revision << "\""; + if (sector_size) { + prop << ",\n \"block_size\": \"" << sector_size << "\""; + } + prop << "\n}\n"; + + if (prop.fail()) { + cerr << "Error: Can't create properties file '" + properties_file + "': " << strerror(errno) << endl; + } + else { + cout << "Created properties file '" + properties_file + "'\n" << flush; + } +} + +bool S2pDump::SetLogLevel() const +{ + const level::level_enum l = level::from_str(log_level); + // Compensate for spdlog using 'off' for unknown levels + if (to_string_view(l) != log_level) { + return false; + } + + set_level(l); + + return true; +} + diff --git a/cpp/s2pdump/s2pdump_core.h b/cpp/s2pdump/s2pdump_core.h new file mode 100644 index 00000000..11d26663 --- /dev/null +++ b/cpp/s2pdump/s2pdump_core.h @@ -0,0 +1,124 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "buses/bus_factory.h" +#include "s2pdump/s2pdump_executor.h" +#include +#include +#include +#include + +using namespace std; + +class S2pDump +{ + +public: + + S2pDump() = default; + ~S2pDump() = default; + + int run(span, bool = false); + + struct inquiry_info + { + string vendor; + string product; + string revision; + uint32_t sector_size; + uint64_t capacity; + + void GeneratePropertiesFile(const string&) const; + }; + using inquiry_info_t = struct inquiry_info; + +private: + + bool Banner(span) const; + bool Init(bool); + void ParseArguments(span); + void DisplayBoardId() const; + string ReadWrite(ostream&, fstream&, int, uint32_t, int); + long CalculateEffectiveSize() const; + void ScanBus(); + bool DisplayInquiry(bool); + string DumpRestore(); + bool GetDeviceInfo(); + + bool SetLogLevel() const; + + void Reset() const; + + void CleanUp() const; + static void TerminationHandler(int); + + unique_ptr bus_factory; + + unique_ptr bus; + + unique_ptr scsi_executor; + + inquiry_info_t inq_info; + + vector buffer; + + int initiator_id = 7; + int target_id = -1; + int target_lun = 0; + + string filename; + + string log_level = "info"; + + bool to_stdout = false; + + bool run_inquiry = false; + + bool run_bus_scan = false; + + bool scan_all_luns = false; + + bool restore = false; + + bool create_properties_file = false; + + // Required for the termination handler + static inline S2pDump *instance; + + static const int MINIMUM_BUFFER_SIZE = 1024 * 64; + static const int DEFAULT_BUFFER_SIZE = 1024 * 1024; + + static inline const string DIVIDER = "----------------------------------------"; + + static inline const unordered_map DEVICE_TYPES = { + { byte { 0 }, "Direct Access" }, + { byte { 1 }, "Sequential Access" }, + { byte { 2 }, "Printer" }, + { byte { 3 }, "Processor" }, + { byte { 4 }, "Write-Once" }, + { byte { 5 }, "CD-ROM/DVD/BD/DVD-RAM" }, + { byte { 6 }, "Scanner" }, + { byte { 7 }, "Optical Memory" }, + { byte { 8 }, "Media Changer" }, + { byte { 9 }, "Communications" }, + { byte { 10 }, "Graphic Arts Pre-Press" }, + { byte { 11 }, "Graphic Arts Pre-Press" }, + { byte { 12 }, "Storage Array Controller" }, + { byte { 13 }, "Enclosure Services" }, + { byte { 14 }, "Simplified Direct Access" }, + { byte { 15 }, "Optical Card Reader/Writer" }, + { byte { 16 }, "Bridge Controller" }, + { byte { 17 }, "Object-based Storage" }, + { byte { 18 }, "Automation/Drive Interface" }, + { byte { 19 }, "Security Manager" }, + { byte { 20 }, "Host Managed Zoned Block" }, + { byte { 30 }, "Well Known Logical Unit" } + }; +}; diff --git a/cpp/s2pdump/s2pdump_executor.cpp b/cpp/s2pdump/s2pdump_executor.cpp new file mode 100644 index 00000000..c6d415fa --- /dev/null +++ b/cpp/s2pdump/s2pdump_executor.cpp @@ -0,0 +1,128 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "s2pdump/s2pdump_executor.h" +#include +#include + +using namespace std; +using namespace spdlog; +using namespace scsi_defs; + +void S2pDumpExecutor::TestUnitReady() const +{ + vector cdb(6); + + phase_executor->Execute(scsi_command::cmd_test_unit_ready, cdb, { }, 0); +} + +bool S2pDumpExecutor::Inquiry(span buffer) +{ + vector cdb(6); + cdb[3] = static_cast(buffer.size() >> 8); + cdb[4] = static_cast(buffer.size()); + + return phase_executor->Execute(scsi_command::cmd_inquiry, cdb, buffer, buffer.size()); +} + +pair S2pDumpExecutor::ReadCapacity() +{ + vector buffer(14); + vector cdb(10); + + if (!phase_executor->Execute(scsi_command::cmd_read_capacity10, cdb, buffer, 8)) { + return {0, 0}; + } + + uint64_t capacity = GetInt32(buffer); + + int sector_size_offset = 4; + + if (static_cast(capacity) == -1) { + cdb.resize(16); + // READ CAPACITY(16), not READ LONG(16) + cdb[1] = 0x10; + + if (!phase_executor->Execute(scsi_command::cmd_read_capacity16_read_long16, cdb, buffer, buffer.size())) { + return {0, 0}; + } + + capacity = GetInt64(buffer); + + sector_size_offset = 8; + } + + const uint32_t sector_size = GetInt32(buffer, sector_size_offset); + + return {capacity + 1, sector_size}; +} + +bool S2pDumpExecutor::ReadWrite(span buffer, uint32_t bstart, uint32_t blength, int length, bool isWrite) +{ + vector cdb(10); + cdb[2] = static_cast(bstart >> 24); + cdb[3] = static_cast(bstart >> 16); + cdb[4] = static_cast(bstart >> 8); + cdb[5] = static_cast(bstart); + cdb[7] = static_cast(blength >> 8); + cdb[8] = static_cast(blength); + + return phase_executor->Execute(isWrite ? scsi_command::cmd_write10 : scsi_command::cmd_read10, cdb, buffer, length); +} + +void S2pDumpExecutor::SynchronizeCache() +{ + vector cdb(10); + + phase_executor->Execute(scsi_command::cmd_synchronize_cache10, cdb, { }, 0); +} + +set S2pDumpExecutor::ReportLuns() +{ + vector buffer(512); + vector cdb(12); + cdb[8] = static_cast(buffer.size() >> 8); + cdb[9] = static_cast(buffer.size()); + + // Assume 8 LUNs in case REPORT LUNS is not available + if (!phase_executor->Execute(scsi_command::cmd_reportLuns, cdb, buffer, buffer.size())) { + spdlog::trace("Target does not support REPORT LUNS"); + return {0, 1, 2, 3, 4, 5, 6, 7}; + } + + const auto lun_count = (static_cast(buffer[2]) << 8) | static_cast(buffer[3]) / 8; + spdlog::trace("Target reported LUN count of " + to_string(lun_count)); + + set luns; + int offset = 8; + for (size_t i = 0; i < lun_count && static_cast(offset) + 8 < buffer.size(); i++, offset += 8) { + const uint64_t lun = GetInt64(buffer, offset); + if (lun < 32) { + luns.insert(static_cast(lun)); + } + else { + spdlog::trace("Target reported invalid LUN " + to_string(lun)); + } + } + + return luns; +} + +uint32_t S2pDumpExecutor::GetInt32(span buf, int offset) +{ + return (static_cast(buf[offset]) << 24) | (static_cast(buf[offset + 1]) << 16) | + (static_cast(buf[offset + 2]) << 8) | static_cast(buf[offset + 3]); +} + +uint64_t S2pDumpExecutor::GetInt64(span buf, int offset) +{ + return (static_cast(buf[offset]) << 56) | (static_cast(buf[offset + 1]) << 48) | + (static_cast(buf[offset + 2]) << 40) | (static_cast(buf[offset + 3]) << 32) | + (static_cast(buf[offset + 4]) << 24) | (static_cast(buf[offset + 5]) << 16) | + (static_cast(buf[offset + 6]) << 8) | static_cast(buf[offset + 7]); +} diff --git a/cpp/s2pdump/s2pdump_executor.h b/cpp/s2pdump/s2pdump_executor.h new file mode 100644 index 00000000..6c59f6d0 --- /dev/null +++ b/cpp/s2pdump/s2pdump_executor.h @@ -0,0 +1,47 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "shared/phase_executor.h" +#include +#include +#include + +using namespace std; + +class S2pDumpExecutor +{ + +public: + + S2pDumpExecutor(BUS &bus, int id) + { + phase_executor = make_unique(bus, id); + } + ~S2pDumpExecutor() = default; + + void TestUnitReady() const; + bool Inquiry(span); + pair ReadCapacity(); + bool ReadWrite(span, uint32_t, uint32_t, int, bool); + void SynchronizeCache(); + set ReportLuns(); + + void SetTarget(int id, int lun) + { + phase_executor->SetTarget(id, lun); + } + +private: + + static uint32_t GetInt32(span, int = 0); + static uint64_t GetInt64(span, int = 0); + + unique_ptr phase_executor; +}; diff --git a/cpp/s2pexec/s2pexec.cpp b/cpp/s2pexec/s2pexec.cpp new file mode 100644 index 00000000..b802fe66 --- /dev/null +++ b/cpp/s2pexec/s2pexec.cpp @@ -0,0 +1,18 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "s2pexec_core.h" + +using namespace std; + +int main(int argc, char *argv[]) +{ + vector args(argv, argv + argc); + + return ScsiExec().run(args); +} diff --git a/cpp/s2pexec/s2pexec_core.cpp b/cpp/s2pexec/s2pexec_core.cpp new file mode 100644 index 00000000..60ea3d24 --- /dev/null +++ b/cpp/s2pexec/s2pexec_core.cpp @@ -0,0 +1,293 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "s2pexec_core.h" +#include "buses/bus_factory.h" +#include "controllers/controller_factory.h" +#include "shared/s2p_util.h" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace filesystem; +using namespace google::protobuf; +using namespace google::protobuf::util; +using namespace spdlog; +using namespace scsi_defs; +using namespace s2p_util; + +void ScsiExec::CleanUp() const +{ + if (bus) { + bus->CleanUp(); + } +} + +void ScsiExec::TerminationHandler(int) +{ + instance->bus->SetRST(true); + + instance->CleanUp(); + + // Process will terminate automatically +} + +bool ScsiExec::Banner(span args) const +{ + cout << s2p_util::Banner("(SCSI Command Execution Tool)"); + + if (args.size() > 1 && (string(args[1]) == "-h" || string(args[1]) == "--help")) { + cout << "Usage: " << args[0] << " -t ID[:LUN] [-i BID] [-f INPUT_FILE] [-o OUTPUT_FILE]" + << " [-L LOG_LEVEL] [-b] [-B] [-F] [-T] [-X]\n" + << " ID is the target device ID (0-" << (ControllerFactory::GetIdMax() - 1) << ").\n" + << " LUN is the optional target device LUN (0-" << (ControllerFactory::GetScsiLunMax() - 1) << ")." + << " Default is 0.\n" + << " BID is the board ID (0-7). Default is 7.\n" + << " INPUT_FILE is the protobuf data input file, by default in JSON format.\n" + << " OUTPUT_FILE is the protobuf data output file, by default in JSON format.\n" + << " LOG_LEVEL is the log level {trace|debug|info|warn|err|off}, default is 'info'.\n" + << " -b Signals that the input file is in protobuf binary format.\n" + << " -F Signals that the input file is in protobuf text format.\n" + << " -B Generate a protobuf binary format file.\n" + << " -T Generate a protobuf text format file.\n" + << " -X Shut down s2p.\n" + << flush; + + return false; + } + + return true; +} + +bool ScsiExec::Init(bool) +{ + instance = this; + // Signal handler for cleaning up + struct sigaction termination_handler; + termination_handler.sa_handler = TerminationHandler; + sigemptyset(&termination_handler.sa_mask); + termination_handler.sa_flags = 0; + sigaction(SIGINT, &termination_handler, nullptr); + sigaction(SIGTERM, &termination_handler, nullptr); + signal(SIGPIPE, SIG_IGN); + + bus_factory = make_unique(); + + bus = bus_factory->CreateBus(BUS::mode_e::INITIATOR); + if (bus) { + scsi_executor = make_unique(*bus, initiator_id); + } + + return bus != nullptr; +} + +void ScsiExec::ParseArguments(span args) +{ + optind = 1; + opterr = 0; + int opt; + while ((opt = getopt(static_cast(args.size()), args.data(), "i:f:t:bo:L:BFTX")) != -1) { + switch (opt) { + case 'i': + if (!GetAsUnsignedInt(optarg, initiator_id) || initiator_id > 7) { + throw parser_exception("Invalid board ID " + to_string(initiator_id) + " (0-7)"); + } + break; + + case 'b': + input_format = S2pDumpExecutor::protobuf_format::binary; + break; + + case 'F': + input_format = S2pDumpExecutor::protobuf_format::text; + break; + + case 'B': + output_format = S2pDumpExecutor::protobuf_format::binary; + break; + + case 'T': + output_format = S2pDumpExecutor::protobuf_format::text; + break; + + case 'f': + input_filename = optarg; + break; + + case 'o': + output_filename = optarg; + if (output_filename.empty()) { + throw parser_exception("Missing output filename"); + } + break; + + case 't': + if (const string error = ProcessId(ControllerFactory::GetIdMax(), ControllerFactory::GetLunMax(), + optarg, target_id, target_lun); !error.empty()) { + throw parser_exception(error); + } + break; + + case 'L': + log_level = optarg; + break; + + case 'X': + shut_down = true; + break; + + default: + break; + } + } + + if (target_id == -1) { + throw parser_exception("Missing target ID"); + } + + if (target_lun == -1) { + target_lun = 0; + } + + if (shut_down) { + return; + } + + if (input_filename.empty()) { + throw parser_exception("Missing input filename"); + } +} + +int ScsiExec::run(span args, bool in_process) +{ + if (!Banner(args)) { + return EXIT_SUCCESS; + } + + try { + ParseArguments(args); + } + catch (const parser_exception &e) { + cerr << "Error: " << e.what() << endl; + return EXIT_FAILURE; + } + + if (target_id == initiator_id) { + cerr << "Target ID and board ID must not be identical" << endl; + return EXIT_FAILURE; + } + + if (!Init(in_process)) { + cerr << "Error: Can't initialize bus" << endl; + return EXIT_FAILURE; + } + + if (!in_process && !bus_factory->IsRaspberryPi()) { + cerr << "Error: No board hardware support" << endl; + return EXIT_FAILURE; + } + + if (!SetLogLevel()) { + cerr << "Error: Invalid log level '" + log_level + "'"; + return EXIT_FAILURE; + } + + scsi_executor->SetTarget(target_id, target_lun); + + if (shut_down) { + const bool status = scsi_executor->ShutDown(); + + CleanUp(); + + return status ? EXIT_SUCCESS : EXIT_FAILURE; + } + + PbResult result; + if (string error = scsi_executor->Execute(input_filename, input_format, result); !error.empty()) { + cerr << "Error: " << error << endl; + + CleanUp(); + + return EXIT_FAILURE; + } + + if (output_filename.empty()) { + string json; + MessageToJsonString(result, &json); + cout << json << '\n'; + + CleanUp(); + + return EXIT_SUCCESS; + } + + switch (output_format) { + case S2pDumpExecutor::protobuf_format::binary: { + ofstream out(output_filename, ios::binary); + if (out.fail()) { + cerr << "Error: " << "Can't open protobuf binary output file '" << output_filename << "'" << endl; + } + + const string data = result.SerializeAsString(); + out.write(data.data(), data.size()); + break; + } + + case S2pDumpExecutor::protobuf_format::json: { + ofstream out(output_filename); + if (out.fail()) { + cerr << "Error: " << "Can't open protobuf JSON output file '" << output_filename << "'" << endl; + } + + string json; + MessageToJsonString(result, &json); + out << json << '\n'; + break; + } + + case S2pDumpExecutor::protobuf_format::text: { + ofstream out(output_filename); + if (out.fail()) { + cerr << "Error: " << "Can't open protobuf text format output file '" << output_filename << "'" << endl; + } + + string text; + TextFormat::PrintToString(result, &text); + out << text << '\n'; + break; + } + + default: + assert(false); + break; + } + + CleanUp(); + + return EXIT_SUCCESS; +} + +bool ScsiExec::SetLogLevel() const +{ + const level::level_enum l = level::from_str(log_level); + // Compensate for spdlog using 'off' for unknown levels + if (to_string_view(l) != log_level) { + return false; + } + + set_level(l); + + return true; +} + diff --git a/cpp/s2pexec/s2pexec_core.h b/cpp/s2pexec/s2pexec_core.h new file mode 100644 index 00000000..790c360b --- /dev/null +++ b/cpp/s2pexec/s2pexec_core.h @@ -0,0 +1,62 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "buses/bus_factory.h" +#include "s2pexec/s2pexec_executor.h" +#include +#include +#include + +using namespace std; + +class ScsiExec +{ + +public: + + ScsiExec() = default; + ~ScsiExec() = default; + + int run(span, bool = false); + +private: + + bool Banner(span) const; + bool Init(bool); + void ParseArguments(span); + + bool SetLogLevel() const; + + void CleanUp() const; + static void TerminationHandler(int); + + unique_ptr bus_factory; + + unique_ptr bus; + + unique_ptr scsi_executor; + + int initiator_id = 7; + int target_id = -1; + int target_lun = 0; + + string input_filename; + string output_filename; + + S2pDumpExecutor::protobuf_format input_format = S2pDumpExecutor::protobuf_format::json; + S2pDumpExecutor::protobuf_format output_format = S2pDumpExecutor::protobuf_format::json; + + bool shut_down = false; + + string log_level = "info"; + + // Required for the termination handler + static inline ScsiExec *instance; +}; diff --git a/cpp/s2pexec/s2pexec_executor.cpp b/cpp/s2pexec/s2pexec_executor.cpp new file mode 100644 index 00000000..7b1c4a05 --- /dev/null +++ b/cpp/s2pexec/s2pexec_executor.cpp @@ -0,0 +1,117 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "s2pexec/s2pexec_executor.h" +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace filesystem; +using namespace google::protobuf; +using namespace google::protobuf::util; +using namespace scsi_defs; +using namespace command_interface; + +string S2pDumpExecutor::Execute(const string& filename, protobuf_format input_format, PbResult& result) +{ + int length = 0; + + switch (input_format) { + case protobuf_format::binary: { + ifstream in(filename, ios::binary); + if (in.fail()) { + return "Can't open input file '" + filename + "': " + strerror(errno); + } + + length = file_size(filename); + vector data(length); + in.read(data.data(), length); + memcpy(buffer.data(), data.data(), length); + break; + } + + case protobuf_format::json: + case protobuf_format::text: { + ifstream in(filename); + if (in.fail()) { + return "Can't open input file '" + filename + "': " + strerror(errno); + } + + stringstream buf; + buf << in.rdbuf(); + const string data = buf.str(); + length = data.size(); + memcpy(buffer.data(), data.data(), length); + break; + } + + default: + assert(false); + break; + } + + array cdb = { }; + cdb[1] = static_cast(input_format); + cdb[7] = static_cast(length >> 8); + cdb[8] = static_cast(length); + + if (!phase_executor->Execute(scsi_command::cmd_execute_operation, cdb, buffer, length)) { + return "Can't execute operation"; + } + + cdb[7] = static_cast(buffer.size() >> 8); + cdb[8] = static_cast(buffer.size()); + + if (!phase_executor->Execute(scsi_command::cmd_receive_operation_results, cdb, buffer, buffer.size())) { + return "Can't read operation result"; + } + + switch (input_format) { + case protobuf_format::binary: { + if (!result.ParseFromArray(buffer.data(), phase_executor->GetByteCount())) { + return "Can't parse binary protobuf data"; + } + break; + } + + case protobuf_format::json: { + const string json((const char*) buffer.data(), phase_executor->GetByteCount()); + if (!JsonStringToMessage(json, &result).ok()) { + return "Can't parse JSON protobuf data"; + } + break; + } + + case protobuf_format::text: { + const string text((const char*) buffer.data(), phase_executor->GetByteCount()); + if (!TextFormat::ParseFromString(text, &result)) { + return "Can't parse text format protobuf data"; + } + break; + } + + default: + assert(false); + break; + } + + return ""; +} + +bool S2pDumpExecutor::ShutDown() +{ + array cdb = { }; + + phase_executor->Execute(scsi_command::cmd_start_stop, cdb, buffer, 0); + + return true; +} diff --git a/cpp/s2pexec/s2pexec_executor.h b/cpp/s2pexec/s2pexec_executor.h new file mode 100644 index 00000000..9d7842e8 --- /dev/null +++ b/cpp/s2pexec/s2pexec_executor.h @@ -0,0 +1,53 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "shared/phase_executor.h" +#include "generated/command_interface.pb.h" +#include +#include +#include + +using namespace std; +using namespace command_interface; + +class S2pDumpExecutor +{ + + // The SCSI ExecuteOperation command supports a byte count of up to 65535 bytes + inline static const int BUFFER_SIZE = 65535; + +public: + + enum class protobuf_format { + binary = 0b001, + json = 0b010, + text = 0b100 + }; + + S2pDumpExecutor(BUS &bus, int id) + { + phase_executor = make_unique(bus, id); + } + ~S2pDumpExecutor() = default; + + string Execute(const string&, protobuf_format, PbResult&); + bool ShutDown(); + + void SetTarget(int id, int lun) + { + phase_executor->SetTarget(id, lun); + } + +private: + + array buffer; + + unique_ptr phase_executor; +}; diff --git a/cpp/shared/network_util.cpp b/cpp/shared/network_util.cpp new file mode 100644 index 00000000..ee8dce43 --- /dev/null +++ b/cpp/shared/network_util.cpp @@ -0,0 +1,74 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "network_util.h" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +bool network_util::IsInterfaceUp(const string& interface) +{ + ifreq ifr = {}; + strncpy(ifr.ifr_name, interface.c_str(), IFNAMSIZ - 1); //NOSONAR Using strncpy is safe + const int fd = socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP); + + if (!ioctl(fd, SIOCGIFFLAGS, &ifr) && (ifr.ifr_flags & IFF_UP)) { + close(fd); + return true; + } + + close(fd); + return false; +} + +set> network_util::GetNetworkInterfaces() +{ + set> network_interfaces; + +#ifdef __linux__ + ifaddrs *addrs; + getifaddrs(&addrs); + ifaddrs *tmp = addrs; + + while (tmp) { + if (const string name = tmp->ifa_name; tmp->ifa_addr && tmp->ifa_addr->sa_family == AF_PACKET && + name != "lo" && name != "piscsi_bridge" && !name.starts_with("dummy") && IsInterfaceUp(name)) { + // Only list interfaces that are up + network_interfaces.insert(name); + } + + tmp = tmp->ifa_next; + } + + freeifaddrs(addrs); +#endif + + return network_interfaces; +} + +bool network_util::ResolveHostName(const string& host, sockaddr_in *addr) +{ + addrinfo hints = {}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + + if (addrinfo *result; !getaddrinfo(host.c_str(), nullptr, &hints, &result)) { + *addr = *reinterpret_cast(result->ai_addr); //NOSONAR bit_cast is not supported by the bullseye compiler + freeaddrinfo(result); + return true; + } + + return false; +} diff --git a/cpp/shared/network_util.h b/cpp/shared/network_util.h new file mode 100644 index 00000000..f58accdb --- /dev/null +++ b/cpp/shared/network_util.h @@ -0,0 +1,23 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include +#include + +using namespace std; + +struct sockaddr_in; + +namespace network_util +{ + bool IsInterfaceUp(const string&); + set> GetNetworkInterfaces(); + bool ResolveHostName(const string&, sockaddr_in *); +} diff --git a/cpp/shared/phase_executor.cpp b/cpp/shared/phase_executor.cpp new file mode 100644 index 00000000..2b997e4f --- /dev/null +++ b/cpp/shared/phase_executor.cpp @@ -0,0 +1,272 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "buses/bus.h" +#include "shared/phase_executor.h" +#include +#include +#include +#include +#include + +using namespace std; +using namespace spdlog; + +void PhaseExecutor::Reset() const +{ + bus.SetDAT(0); + bus.SetBSY(false); + bus.SetSEL(false); + bus.SetATN(false); +} + +bool PhaseExecutor::Execute(scsi_command cmd, span cdb, span buffer, int length) +{ + status = 0; + byte_count = 0; + + spdlog::trace( + fmt::format("Executing {0} for target {1}:{2}", command_mapping.find(cmd)->second.second, target_id, + target_lun)); + + if (!Arbitration()) { + bus.Reset(); + return false; + } + + if (!Selection()) { + Reset(); + return false; + } + + // Timeout 3 s + auto now = chrono::steady_clock::now(); + while ((chrono::duration_cast(chrono::steady_clock::now() - now).count()) < 3) { + bus.Acquire(); + + if (bus.GetREQ()) { + try { + if (Dispatch(cmd, cdb, buffer, static_cast(length))) { + now = chrono::steady_clock::now(); + } + else { + bus.Reset(); + return !GetStatus(); + } + } + catch (const phase_exception &e) { + cerr << "Error: " << e.what() << endl; + bus.Reset(); + return false; + } + } + } + + return false; +} + +bool PhaseExecutor::Dispatch(scsi_command cmd, span cdb, span buffer, int length) +{ + const phase_t phase = bus.GetPhase(); + + spdlog::trace(string("Handling ") + BUS::GetPhaseName(phase) + " phase"); + + switch (phase) { + case phase_t::command: + Command(cmd, cdb); + break; + + case phase_t::status: + Status(); + break; + + case phase_t::datain: + DataIn(buffer, length); + break; + + case phase_t::dataout: + DataOut(buffer, length); + break; + + case phase_t::msgin: + MsgIn(); + // Done with this command cycle + return false; + + case phase_t::msgout: + MsgOut(); + break; + + default: + throw phase_exception(string("Ignoring ") + BUS::GetPhaseName(phase) + " phase"); + break; + } + + return true; +} + +bool PhaseExecutor::Arbitration() const +{ + if (!WaitForFree()) { + spdlog::trace("Bus is not free"); + return false; + } + + Sleep(BUS_FREE_DELAY); + + bus.SetDAT(static_cast(1 << initiator_id)); + + bus.SetBSY(true); + + Sleep(ARBITRATION_DELAY); + + if (bus.GetDAT() > (1 << initiator_id)) { + spdlog::trace( + fmt::format("Lost ARBITRATION, competing initiator ID is {}", bus.GetDAT() - (1 << initiator_id))); + return false; + } + + bus.SetSEL(true); + + Sleep(BUS_CLEAR_DELAY); + Sleep(BUS_SETTLE_DELAY); + + return true; +} + +bool PhaseExecutor::Selection() const +{ + bus.SetDAT(static_cast((1 << initiator_id) + (1 << target_id))); + + bus.SetSEL(true); + + // Request MESSAGE OUT for IDENTIFY + bus.SetATN(true); + + Sleep(DESKEW_DELAY); + Sleep(DESKEW_DELAY); + + bus.SetBSY(false); + + Sleep(BUS_SETTLE_DELAY); + + if (!WaitForBusy()) { + spdlog::trace("SELECTION failed"); + return false; + } + + Sleep(DESKEW_DELAY); + Sleep(DESKEW_DELAY); + + bus.SetSEL(false); + + return true; +} + +void PhaseExecutor::Command(scsi_command cmd, span cdb) const +{ + cdb[0] = static_cast(cmd); + if (target_lun < 8) { + // Encode LUN in the CDB for backwards compatibility with SCSI-1-CCS + cdb[1] = static_cast(cdb[1] + (target_lun << 5)); + } + + if (static_cast(cdb.size()) != + bus.SendHandShake(cdb.data(), static_cast(cdb.size()))) { + throw phase_exception(command_mapping.find(cmd)->second.second + string(" failed")); + } +} + +void PhaseExecutor::Status() +{ + array buf; + + if (bus.ReceiveHandShake(buf.data(), 1) != static_cast(buf.size())) { + throw phase_exception("STATUS failed"); + } + + status = buf[0]; +} + +void PhaseExecutor::DataIn(span buffer, int length) +{ + byte_count = bus.ReceiveHandShake(buffer.data(), length); + if (!byte_count) { + throw phase_exception("DATA IN failed"); + } +} + +void PhaseExecutor::DataOut(span buffer, int length) +{ + if (bus.SendHandShake(buffer.data(), length) != length) { + throw phase_exception("DATA OUT failed"); + } +} + +void PhaseExecutor::MsgIn() const +{ + array buf; + + if (bus.ReceiveHandShake(buf.data(), buf.size()) != buf.size()) { + throw phase_exception("MESSAGE IN failed"); + } + + if (buf[0]) { + throw phase_exception("MESSAGE IN did not report COMMAND COMPLETE"); + } +} + +void PhaseExecutor::MsgOut() const +{ + array buf; + + // IDENTIFY + buf[0] = static_cast(target_lun | 0x80); + + if (bus.SendHandShake(buf.data(), buf.size()) != buf.size()) { + throw phase_exception("MESSAGE OUT for IDENTIFY failed"); + } +} + +bool PhaseExecutor::WaitForFree() const +{ + // Wait for up to 2 s + int count = 10'000; + do { + // Wait 20 ms + Sleep( { .tv_sec = 0, .tv_nsec = 20'000 }); + bus.Acquire(); + if (!bus.GetBSY() && !bus.GetSEL()) { + return true; + } + } while (count--); + + return false; +} + +bool PhaseExecutor::WaitForBusy() const +{ + // Wait for up to 2 s + int count = 10'000; + do { + // Wait 20 ms + Sleep( { .tv_sec = 0, .tv_nsec = 20'000 }); + bus.Acquire(); + if (bus.GetBSY()) { + return true; + } + } while (count--); + + return false; +} + +void PhaseExecutor::SetTarget(int id, int lun) +{ + target_id = id; + target_lun = lun; +} diff --git a/cpp/shared/phase_executor.h b/cpp/shared/phase_executor.h new file mode 100644 index 00000000..99a96389 --- /dev/null +++ b/cpp/shared/phase_executor.h @@ -0,0 +1,96 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "buses/bus.h" +#include +#include +#include + +using namespace std; +using namespace scsi_defs; + +class PhaseExecutor +{ + class phase_exception: public runtime_error + { + using runtime_error::runtime_error; + }; + +public: + + PhaseExecutor(BUS &b, int id) : bus(b), initiator_id(id) + { + } + ~PhaseExecutor() = default; + + void SetTarget(int, int); + bool Execute(scsi_command, span, span, int); + + int GetByteCount() const + { + return byte_count; + } + +private: + + bool Dispatch(scsi_command, span, span, int); + + void Reset() const; + + bool Arbitration() const; + bool Selection() const; + void Command(scsi_command, span) const; + void Status(); + void DataIn(span, int); + void DataOut(span, int); + void MsgIn() const; + void MsgOut() const; + + bool WaitForFree() const; + bool WaitForBusy() const; + + int GetStatus() const + { + return status; + } + + inline void Sleep(const timespec &ns) const + { + nanosleep(&ns, nullptr); + } + + BUS &bus; + + int initiator_id; + + int target_id = -1; + int target_lun = -1; + + int status = -1; + + int byte_count = 0; + + // Timeout values see bus.h + + inline static const long BUS_SETTLE_DELAY_NS = 400; + inline static const timespec BUS_SETTLE_DELAY = { .tv_sec = 0, .tv_nsec = BUS_SETTLE_DELAY_NS }; + + inline static const long BUS_CLEAR_DELAY_NS = 800; + inline static const timespec BUS_CLEAR_DELAY = { .tv_sec = 0, .tv_nsec = BUS_CLEAR_DELAY_NS }; + + inline static const long BUS_FREE_DELAY_NS = 800; + inline static const timespec BUS_FREE_DELAY = { .tv_sec = 0, .tv_nsec = BUS_FREE_DELAY_NS }; + + inline static const long DESKEW_DELAY_NS = 45; + inline static const timespec DESKEW_DELAY = { .tv_sec = 0, .tv_nsec = DESKEW_DELAY_NS }; + + inline static const long ARBITRATION_DELAY_NS = 2'400; + inline static const timespec ARBITRATION_DELAY = { .tv_sec = 0, .tv_nsec = ARBITRATION_DELAY_NS }; +}; diff --git a/cpp/shared/protobuf_util.cpp b/cpp/shared/protobuf_util.cpp new file mode 100644 index 00000000..dbc4d028 --- /dev/null +++ b/cpp/shared/protobuf_util.cpp @@ -0,0 +1,237 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/shared_exceptions.h" +#include "s2p_util.h" +#include "protobuf_util.h" +#include +#include +#include +#include +#include + +using namespace std; +using namespace s2p_util; +using namespace command_interface; + +#define FPRT(fp, ...) fprintf(fp, __VA_ARGS__ ) + +void protobuf_util::ParseParameters(PbDeviceDefinition& device, const string& params) +{ + if (params.empty()) { + return; + } + + // Old style parameter (filename), for backwards compatibility only + if (params.find(KEY_VALUE_SEPARATOR) == string::npos) { + SetParam(device, "file", params); + + return; + } + + for (const auto& p : Split(params, COMPONENT_SEPARATOR)) { + if (const auto& param = Split(p, KEY_VALUE_SEPARATOR, 2); param.size() == 2) { + SetParam(device, param[0], param[1]); + } + } +} + +string protobuf_util::SetCommandParams(PbCommand& command, const string& params) +{ + if (params.find(KEY_VALUE_SEPARATOR) != string::npos) { + return SetFromGenericParams(command, params); + } + + string folder_pattern; + string file_pattern; + string operations; + + switch (const auto& components = Split(params, COMPONENT_SEPARATOR, 3); components.size()) { + case 3: + operations = components[2]; + [[fallthrough]]; + + case 2: + folder_pattern = components[0]; + file_pattern = components[1]; + break; + + case 1: + file_pattern = components[0]; + break; + + default: + break; + } + + SetParam(command, "folder_pattern", folder_pattern); + SetParam(command, "file_pattern", file_pattern); + SetParam(command, "operations", operations); + + return ""; +} + +string protobuf_util::SetFromGenericParams(PbCommand& command, const string& params) +{ + for (const string& key_value : Split(params, COMPONENT_SEPARATOR)) { + const auto& param = Split(key_value, KEY_VALUE_SEPARATOR, 2); + if (param.size() > 1 && !param[0].empty()) { + SetParam(command, param[0], param[1]); + } + else { + return "Parameter '" + key_value + "' has to be a key/value pair"; + } + } + + return ""; +} + +void protobuf_util::SetProductData(PbDeviceDefinition& device, const string& data) +{ + const auto& components = Split(data, COMPONENT_SEPARATOR, 3); + switch (components.size()) { + case 3: + device.set_revision(components[2]); + [[fallthrough]]; + + case 2: + device.set_product(components[1]); + [[fallthrough]]; + + case 1: + device.set_vendor(components[0]); + break; + + default: + break; + } +} + +string protobuf_util::SetIdAndLun(int id_max, int lun_max, PbDeviceDefinition& device, const string& value) +{ + int id; + int lun; + if (const string error = ProcessId(id_max, lun_max, value, id, lun); !error.empty()) { + return error; + } + + device.set_id(id); + device.set_unit(lun != -1 ? lun : 0); + + return ""; +} + +string protobuf_util::ListDevices(const vector& pb_devices) +{ + if (pb_devices.empty()) { + return "No devices currently attached.\n"; + } + + ostringstream s; + s << "+----+-----+------+-------------------------------------\n" + << "| ID | LUN | TYPE | IMAGE FILE\n" + << "+----+-----+------+-------------------------------------\n"; + + vector devices = pb_devices; + ranges::sort(devices, [](const auto& a, const auto& b) { return a.id() < b.id() || a.unit() < b.unit(); }); + + for (const auto& device : devices) { + string filename; + switch (device.type()) { + case SCDP: + filename = "DaynaPort SCSI/Link"; + break; + + case SCHS: + filename = "Host Services"; + break; + + case SCLP: + filename = "SCSI Printer"; + break; + + default: + filename = device.file().name(); + break; + } + + s << "| " << device.id() << " | " << setw(3) << device.unit() << " | " << PbDeviceType_Name(device.type()) << " | " + << (filename.empty() ? "NO MEDIUM" : filename) + << (!device.status().removed() && (device.properties().read_only() || device.status().protected_()) ? " (READ-ONLY)" : "") + << '\n'; + } + + s << "+----+-----+------+-------------------------------------\n"; + + return s.str(); +} + +//--------------------------------------------------------------------------- +// +// Serialize/Deserialize protobuf message: Length followed by the actual data. +// A little endian platform is assumed. +// +//--------------------------------------------------------------------------- + +void protobuf_util::SerializeMessage(int fd, const google::protobuf::Message& message) +{ + const string data = message.SerializeAsString(); + + // Write the size of the protobuf data as a header + const auto size = static_cast(data.length()); + if (write(fd, &size, sizeof(size)) != sizeof(size)) { + throw io_exception("Can't write protobuf message size"); + } + + // Write the actual protobuf data + if (write(fd, data.data(), size) != size) { + throw io_exception("Can't write protobuf message data"); + } +} + +void protobuf_util::DeserializeMessage(int fd, google::protobuf::Message& message) +{ + // Read the header with the size of the protobuf data + array header_buf; + if (ReadBytes(fd, header_buf) < header_buf.size()) { + throw io_exception("Can't read protobuf message size"); + } + + const int size = (static_cast(header_buf[3]) << 24) + (static_cast(header_buf[2]) << 16) + + (static_cast(header_buf[1]) << 8) + static_cast(header_buf[0]); + if (size < 0) { + throw io_exception("Invalid protobuf message size"); + } + + // Read the binary protobuf data + vector data_buf(size); + if (ReadBytes(fd, data_buf) != data_buf.size()) { + throw io_exception("Invalid protobuf message data"); + } + + message.ParseFromArray(data_buf.data(), size); +} + +size_t protobuf_util::ReadBytes(int fd, span buf) +{ + size_t offset = 0; + while (offset < buf.size()) { + const auto len = read(fd, &buf.data()[offset], buf.size() - offset); + if (len == -1) { + throw io_exception("Read error: " + string(strerror(errno))); + } + + if (!len) { + break; + } + + offset += len; + } + + return offset; +} diff --git a/cpp/shared/protobuf_util.h b/cpp/shared/protobuf_util.h new file mode 100644 index 00000000..b7ce8688 --- /dev/null +++ b/cpp/shared/protobuf_util.h @@ -0,0 +1,49 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +// Helper methods for setting up/evaluating protobuf messages +// +//--------------------------------------------------------------------------- + +#pragma once + +#include +#include +#include +#include "generated/command_interface.pb.h" + +using namespace std; +using namespace command_interface; + +namespace protobuf_util +{ + static const char KEY_VALUE_SEPARATOR = '='; + + string GetParam(const auto& item, const string& key) + { + const auto& it = item.params().find(key); + return it != item.params().end() ? it->second : ""; + } + + void SetParam(auto& item, const string& key, string_view value) + { + if (!key.empty() && !value.empty()) { + auto& map = *item.mutable_params(); + map[key] = value; + } + } + + void ParseParameters(PbDeviceDefinition&, const string&); + string SetCommandParams(PbCommand&, const string&); + string SetFromGenericParams(PbCommand&, const string&); + void SetProductData(PbDeviceDefinition&, const string&); + string SetIdAndLun(int, int, PbDeviceDefinition&, const string&); + string ListDevices(const vector&); + + void SerializeMessage(int, const google::protobuf::Message&); + void DeserializeMessage(int, google::protobuf::Message&); + size_t ReadBytes(int, span); +} diff --git a/cpp/shared/s2p_util.cpp b/cpp/shared/s2p_util.cpp new file mode 100644 index 00000000..de0fbb26 --- /dev/null +++ b/cpp/shared/s2p_util.cpp @@ -0,0 +1,134 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "controllers/controller_factory.h" +#include "s2p_version.h" +#include "s2p_util.h" +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace filesystem; + +string s2p_util::GetVersionString() +{ + return fmt::format("{0}.{1}{2}", s2p_major_version, s2p_minor_version, + s2p_patch_version < 0 ? " --DEVELOPMENT BUILD--" : "." + to_string(s2p_patch_version)); +} + +vector s2p_util::Split(const string& s, char separator, int limit) +{ + assert(limit >= 0); + + string component; + vector result; + stringstream str(s); + + while (--limit > 0 && getline(str, component, separator)) { + result.push_back(component); + } + + if (!str.eof()) { + getline(str, component); + result.push_back(component); + } + + return result; +} + +string s2p_util::GetLocale() +{ + const char *locale = setlocale(LC_MESSAGES, ""); + if (locale == nullptr || !strcmp(locale, "C")) { + locale = "en"; + } + + return locale; +} + +bool s2p_util::GetAsUnsignedInt(const string& value, int& result) +{ + if (value.find_first_not_of("0123456789") != string::npos) { + return false; + } + + try { + auto v = stoul(value); + result = (int)v; + } + catch(const invalid_argument&) { + return false; + } + catch(const out_of_range&) { + return false; + } + + return true; +} + +string s2p_util::ProcessId(int id_max, int lun_max, const string& id_spec, int& id, int& lun) +{ + id = -1; + lun = -1; + + if (id_spec.empty()) { + return "Missing device ID"; + } + + if (const auto& components = Split(id_spec, COMPONENT_SEPARATOR, 2); !components.empty()) { + if (components.size() == 1) { + if (!GetAsUnsignedInt(components[0], id) || id >= id_max) { + id = -1; + + return "Invalid device ID (0-" + to_string(id_max - 1) + ")"; + } + + return ""; + } + + if (!GetAsUnsignedInt(components[0], id) || id >= id_max || !GetAsUnsignedInt(components[1], lun) || lun >= lun_max) { + id = -1; + lun = -1; + + return "Invalid LUN (0-" + to_string(lun_max - 1) + ")"; + } + } + + return ""; +} + +string s2p_util::Banner(string_view app) +{ + stringstream s; + + s << "SCSI Target Emulator and SCSI Initiator Tools SCSI2Pi " << app << "\n" + << "Version " << GetVersionString() << " (" << __DATE__ << ' ' << __TIME__ << ")\n" + << "Copyright (C) 2016-2020 GIMONS\n" + << "Copyright (C) 2020-2023 Contributors to the PiSCSI project\n" + << "Copyright (C) 2023 Uwe Seimet\n"; + + return s.str(); +} + +string s2p_util::GetExtensionLowerCase(string_view filename) +{ + string ext; + ranges::transform(path(filename).extension().string(), back_inserter(ext), ::tolower); + + // Remove the leading dot + return ext.empty() ? "" : ext.substr(1); +} + +void s2p_util::LogErrno(const string& msg) +{ + spdlog::error(errno ? msg + ": " + string(strerror(errno)) : msg); +} diff --git a/cpp/shared/s2p_util.h b/cpp/shared/s2p_util.h new file mode 100644 index 00000000..8a0c674a --- /dev/null +++ b/cpp/shared/s2p_util.h @@ -0,0 +1,56 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include +#include +#include +#include + +using namespace std; + +namespace s2p_util +{ + // Separator for compound options like ID:LUN + static const char COMPONENT_SEPARATOR = ':'; + + struct StringHash { + using is_transparent = void; + + size_t operator()(string_view sv) const { + hash hasher; + return hasher(sv); + } + }; + + string Join(const auto& collection, const string_view separator = ", ") { + ostringstream s; + + for (const auto& element : collection) { + if (s.tellp()) { + s << separator; + } + + s << element; + } + + return s.str(); + } + + string GetVersionString(); + vector Split(const string&, char, int = INT_MAX); + string GetLocale(); + bool GetAsUnsignedInt(const string&, int&); + string ProcessId(int, int, const string&, int&, int&); + string Banner(string_view); + + string GetExtensionLowerCase(string_view); + + void LogErrno(const string&); +} diff --git a/cpp/shared/s2p_version.h b/cpp/shared/s2p_version.h new file mode 100644 index 00000000..68c2d948 --- /dev/null +++ b/cpp/shared/s2p_version.h @@ -0,0 +1,16 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +inline const int s2p_major_version = 1; + +inline const int s2p_minor_version = 0; + +// Patch count, -1 for development version +inline const int s2p_patch_version = -1; diff --git a/cpp/shared/scsi.h b/cpp/shared/scsi.h new file mode 100644 index 00000000..ffdc8002 --- /dev/null +++ b/cpp/shared/scsi.h @@ -0,0 +1,194 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2001-2006 PI.(ytanaka@ipc-tokai.or.jp) +// Copyright (C) 2014-2020 GIMONS +// Copyright (C) 2021-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include +#include +#include + +using namespace std; + +// Command Descriptor Block +using cdb_t = span; + +namespace scsi_defs +{ +enum class scsi_level { + scsi_1_ccs = 1, + scsi_2 = 2, + spc = 3, + spc_2 = 4, + spc_3 = 5, + spc_4 = 6, + spc_5 = 7, + spc_6 = 8 +}; + +// Phase definitions +enum class phase_t { + busfree, + arbitration, + selection, + reselection, + command, + datain, + dataout, + status, + msgin, + msgout, + reserved +}; + +enum class device_type { + direct_access = 0, + printer = 2, + processor = 3, + cd_rom = 5, + optical_memory = 7, + communications = 9 +}; + +enum class scsi_command { + cmd_test_unit_ready = 0x00, + cmd_rezero = 0x01, + cmd_request_sense = 0x03, + cmd_format_unit = 0x04, + cmd_reassign_blocks = 0x07, + cmd_read6 = 0x08, + // Bridge specific command + cmd_getMessage10 = 0x08, + // DaynaPort specific command + cmd_retrieve_stats = 0x09, + cmd_write6 = 0x0a, + // Bridge specific ommand + cmd_sendMessage10 = 0x0a, + cmd_print = 0x0a, + cmd_seek6 = 0x0b, + // DaynaPort specific command + cmd_set_iface_mode = 0x0c, + // DaynaPort specific command + cmd_set_mcast_addr = 0x0d, + // DaynaPort specific command + cmd_enable_interface = 0x0e, + cmd_synchronize_buffer = 0x10, + cmd_inquiry = 0x12, + cmd_mode_select6 = 0x15, + cmd_reserve6 = 0x16, + cmd_release6 = 0x17, + cmd_mode_sense6 = 0x1a, + cmd_start_stop = 0x1b, + cmd_stop_print = 0x1b, + cmd_send_diagnostic = 0x1d, + cmd_prevent_allow_medium_removal = 0x1e, + cmd_read_capacity10 = 0x25, + cmd_read10 = 0x28, + cmd_write10 = 0x2a, + cmd_seek10 = 0x2b, + cmd_verify10 = 0x2f, + cmd_synchronize_cache10 = 0x35, + cmd_read_defect_data10 = 0x37, + cmd_read_long10 = 0x3e, + cmd_write_long10 = 0x3f, + cmd_read_toc = 0x43, + cmd_get_event_status_notification = 0x4a, + cmd_mode_select10 = 0x55, + cmd_mode_sense10 = 0x5a, + cmd_read16 = 0x88, + cmd_write16 = 0x8a, + cmd_verify16 = 0x8f, + cmd_synchronize_cache16 = 0x91, + cmd_read_capacity16_read_long16 = 0x9e, + cmd_write_long16 = 0x9f, + cmd_reportLuns = 0xa0, + // Host services specific commands + cmd_execute_operation = 0xc0, + cmd_receive_operation_results = 0xc1 +}; + +enum class status { + good = 0x00, + check_condition = 0x02, + reservation_conflict = 0x18 +}; + +enum class sense_key { + no_sense = 0x00, + not_ready = 0x02, + medium_error = 0x03, + illegal_request = 0x05, + unit_attention = 0x06, + data_protect = 0x07, + aborted_command = 0x0b +}; + +enum class asc { + no_additional_sense_information = 0x00, + write_fault = 0x03, + read_fault = 0x11, + invalid_command_operation_code = 0x20, + lba_out_of_range = 0x21, + invalid_field_in_cdb = 0x24, + invalid_lun = 0x25, + invalid_field_in_parameter_list = 0x26, + write_protected = 0x27, + not_ready_to_ready_change = 0x28, + power_on_or_reset = 0x29, + medium_not_present = 0x3a, + load_or_eject_failed = 0x53 +}; + +static const unordered_map> command_mapping = { + { scsi_command::cmd_test_unit_ready, make_pair(6, "TestUnitReady") }, + { scsi_command::cmd_rezero, make_pair(6, "Rezero") }, + { scsi_command::cmd_request_sense, make_pair(6, "RequestSense") }, + { scsi_command::cmd_format_unit, make_pair(6, "FormatUnit") }, + { scsi_command::cmd_reassign_blocks, make_pair(6, "ReassignBlocks") }, + { scsi_command::cmd_read6, make_pair(6, "Read6/GetMessage10") }, + { scsi_command::cmd_retrieve_stats, make_pair(6, "RetrieveStats") }, + { scsi_command::cmd_write6, make_pair(6, "Write6/Print/SendMessage10") }, + { scsi_command::cmd_seek6, make_pair(6, "Seek6") }, + { scsi_command::cmd_set_iface_mode, make_pair(6, "SetIfaceMode") }, + { scsi_command::cmd_set_mcast_addr, make_pair(6, "SetMcastAddr") }, + { scsi_command::cmd_enable_interface, make_pair(6, "EnableInterface") }, + { scsi_command::cmd_synchronize_buffer, make_pair(6, "Synchronize_buffer") }, + { scsi_command::cmd_inquiry, make_pair(6, "Inquiry") }, + { scsi_command::cmd_mode_select6, make_pair(6, "ModeSelect6") }, + { scsi_command::cmd_reserve6, make_pair(6, "Reserve6") }, + { scsi_command::cmd_release6, make_pair(6, "Release6") }, + { scsi_command::cmd_mode_sense6, make_pair(6, "ModeSense6") }, + { scsi_command::cmd_start_stop, make_pair(6, "StartStop") }, + { scsi_command::cmd_stop_print, make_pair(6, "StopPrint") }, + { scsi_command::cmd_send_diagnostic, make_pair(6, "SendDiagnostic") }, + { scsi_command::cmd_prevent_allow_medium_removal, make_pair(6, "PreventAllowMediumRemoval") }, + { scsi_command::cmd_read_capacity10, make_pair(10, "ReadCapacity10") }, + { scsi_command::cmd_read10, make_pair(10, "Read10") }, + { scsi_command::cmd_write10, make_pair(10, "Write10") }, + { scsi_command::cmd_seek10, make_pair(10, "Seek10") }, + { scsi_command::cmd_verify10, make_pair(10, "Verify10") }, + { scsi_command::cmd_synchronize_cache10, make_pair(10, "SynchronizeCache10") }, + { scsi_command::cmd_read_defect_data10, make_pair(10, "ReadDefectData10") }, + { scsi_command::cmd_read_long10, make_pair(10, "ReadLong10") }, + { scsi_command::cmd_write_long10, make_pair(10, "WriteLong10") }, + { scsi_command::cmd_read_toc, make_pair(10, "ReadToc") }, + { scsi_command::cmd_get_event_status_notification, make_pair(10, "GetEventStatusNotification") }, + { scsi_command::cmd_mode_select10, make_pair(10, "ModeSelect10") }, + { scsi_command::cmd_mode_sense10, make_pair(10, "ModeSense10") }, + { scsi_command::cmd_read16, make_pair(16, "Read16") }, + { scsi_command::cmd_write16, make_pair(16, "Write16") }, + { scsi_command::cmd_verify16, make_pair(16, "Verify16") }, + { scsi_command::cmd_synchronize_cache16, make_pair(16, "SynchronizeCache16") }, + { scsi_command::cmd_read_capacity16_read_long16, make_pair(16, "ReadCapacity16/ReadLong16") }, + { scsi_command::cmd_write_long16, make_pair(16, "WriteLong16") }, + { scsi_command::cmd_reportLuns, make_pair(12, "ReportLuns") }, + { scsi_command::cmd_execute_operation, make_pair(10, "ExecuteOperation") }, + { scsi_command::cmd_receive_operation_results, make_pair(10, "Receive_operation_results") } +}; +} diff --git a/cpp/shared/shared_exceptions.h b/cpp/shared/shared_exceptions.h new file mode 100644 index 00000000..15519e72 --- /dev/null +++ b/cpp/shared/shared_exceptions.h @@ -0,0 +1,44 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2021-2022 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "scsi.h" +#include + +using namespace std; + +class parser_exception : public runtime_error +{ + using runtime_error::runtime_error; +}; + +class io_exception : public runtime_error +{ + using runtime_error::runtime_error; +}; + +class file_not_found_exception : public io_exception +{ + using io_exception::io_exception; +}; + +class scsi_exception : public exception +{ + scsi_defs::sense_key sense_key; + scsi_defs::asc asc; + +public: + + scsi_exception(scsi_defs::sense_key sense_key, scsi_defs::asc asc = scsi_defs::asc::no_additional_sense_information) + : sense_key(sense_key), asc(asc) {} + ~scsi_exception() override = default; + + scsi_defs::sense_key get_sense_key() const { return sense_key; } + scsi_defs::asc get_asc() const { return asc; } +}; diff --git a/cpp/test/abstract_controller_test.cpp b/cpp/test/abstract_controller_test.cpp new file mode 100644 index 00000000..0d9c4405 --- /dev/null +++ b/cpp/test/abstract_controller_test.cpp @@ -0,0 +1,189 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" +#include "shared/shared_exceptions.h" + +using namespace scsi_defs; + +TEST(AbstractControllerTest, AllocateCmd) +{ + MockAbstractController controller; + + EXPECT_EQ(16, controller.GetCmd().size()); + controller.AllocateCmd(1234); + EXPECT_EQ(1234, controller.GetCmd().size()); +} + +TEST(AbstractControllerTest, SetLength) +{ + MockAbstractController controller; + + EXPECT_EQ(4096, controller.GetBuffer().size()); + controller.SetLength(1); + EXPECT_LE(1, controller.GetBuffer().size()); + controller.SetLength(10000); + EXPECT_LE(10000, controller.GetBuffer().size()); +} + +TEST(AbstractControllerTest, Reset) +{ + auto bus = make_shared(); + auto controller = make_shared(bus, 0); + auto device = make_shared(0); + + controller->AddDevice(device); + + controller->SetPhase(phase_t::status); + EXPECT_EQ(phase_t::status, controller->GetPhase()); + EXPECT_CALL(*bus, Reset()); + controller->Reset(); + EXPECT_TRUE(controller->IsBusFree()); + EXPECT_EQ(status::good, controller->GetStatus()); + EXPECT_EQ(0, controller->GetLength()); +} + +TEST(AbstractControllerTest, Next) +{ + MockAbstractController controller; + + controller.SetNext(0x1234); + EXPECT_EQ(0x1234, controller.GetNext()); + controller.IncrementNext(); + EXPECT_EQ(0x1235, controller.GetNext()); +} + +TEST(AbstractControllerTest, Message) +{ + MockAbstractController controller; + + controller.SetMessage(0x12); + EXPECT_EQ(0x12, controller.GetMessage()); +} + +TEST(AbstractControllerTest, ByteTransfer) +{ + MockAbstractController controller; + + controller.SetByteTransfer(false); + EXPECT_FALSE(controller.IsByteTransfer()); + controller.SetByteTransfer(true); + EXPECT_TRUE(controller.IsByteTransfer()); +} + +TEST(AbstractControllerTest, InitBytesToTransfer) +{ + MockAbstractController controller; + + controller.SetLength(0x1234); + controller.InitBytesToTransfer(); + EXPECT_EQ(0x1234, controller.GetBytesToTransfer()); + controller.SetByteTransfer(false); + EXPECT_EQ(0, controller.GetBytesToTransfer()); +} + +TEST(AbstractControllerTest, GetMaxLuns) +{ + MockAbstractController controller; + + EXPECT_EQ(32, controller.GetMaxLuns()); +} + +TEST(AbstractControllerTest, Status) +{ + MockAbstractController controller; + + controller.SetStatus(status::reservation_conflict); + EXPECT_EQ(status::reservation_conflict, controller.GetStatus()); +} + +TEST(AbstractControllerTest, DeviceLunLifeCycle) +{ + const int ID = 1; + const int LUN = 4; + + auto controller = make_shared>(ID); + + auto device1 = make_shared(LUN); + auto device2 = make_shared(32); + auto device3 = make_shared(-1); + + EXPECT_EQ(0, controller->GetLunCount()); + EXPECT_EQ(ID, controller->GetTargetId()); + EXPECT_TRUE(controller->AddDevice(device1)); + EXPECT_FALSE(controller->AddDevice(device2)); + EXPECT_FALSE(controller->AddDevice(device3)); + EXPECT_TRUE(controller->GetLunCount() > 0); + EXPECT_TRUE(controller->HasDeviceForLun(LUN)); + EXPECT_FALSE(controller->HasDeviceForLun(0)); + EXPECT_NE(nullptr, controller->GetDeviceForLun(LUN)); + EXPECT_EQ(nullptr, controller->GetDeviceForLun(0)); + EXPECT_TRUE(controller->RemoveDevice(*device1)); + EXPECT_EQ(0, controller->GetLunCount()); + EXPECT_FALSE(controller->RemoveDevice(*device1)); +} + +TEST(AbstractControllerTest, GetOpcode) +{ + MockAbstractController controller; + + controller.SetCmdByte(0, static_cast(scsi_command::cmd_inquiry)); + EXPECT_EQ(scsi_command::cmd_inquiry, controller.GetOpcode()); +} + +TEST(AbstractControllerTest, GetLun) +{ + const int LUN = 3; + + MockAbstractController controller; + + controller.SetCmdByte(1, LUN << 5); + EXPECT_EQ(LUN, controller.GetLun()); +} + +TEST(AbstractControllerTest, Blocks) +{ + MockAbstractController controller; + + controller.SetBlocks(1); + EXPECT_TRUE(controller.HasBlocks()); + controller.DecrementBlocks(); + EXPECT_FALSE(controller.HasBlocks()); +} + +TEST(AbstractControllerTest, Length) +{ + MockAbstractController controller; + + EXPECT_FALSE(controller.HasValidLength()); + + controller.SetLength(1); + EXPECT_EQ(1, controller.GetLength()); + EXPECT_TRUE(controller.HasValidLength()); +} + +TEST(AbstractControllerTest, UpdateOffsetAndLength) +{ + MockAbstractController controller; + + EXPECT_FALSE(controller.HasValidLength()); + + controller.UpdateOffsetAndLength(); + EXPECT_EQ(0, controller.GetLength()); +} + +TEST(AbstractControllerTest, Offset) +{ + MockAbstractController controller; + + controller.ResetOffset(); + EXPECT_EQ(0, controller.GetOffset()); + + controller.UpdateOffsetAndLength(); + EXPECT_EQ(0, controller.GetOffset()); +} diff --git a/cpp/test/bus_test.cpp b/cpp/test/bus_test.cpp new file mode 100644 index 00000000..2015dbf3 --- /dev/null +++ b/cpp/test/bus_test.cpp @@ -0,0 +1,128 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" + +TEST(BusTest, GetCommandByteCount) +{ + EXPECT_EQ(43, scsi_defs::command_mapping.size()); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x00)); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x01)); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x03)); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x04)); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x07)); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x08)); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x09)); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x0a)); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x0b)); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x0c)); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x0d)); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x0e)); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x10)); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x12)); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x15)); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x16)); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x17)); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x1a)); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x1b)); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x1d)); + EXPECT_EQ(6, BUS::GetCommandByteCount(0x1e)); + EXPECT_EQ(10, BUS::GetCommandByteCount(0x25)); + EXPECT_EQ(10, BUS::GetCommandByteCount(0x28)); + EXPECT_EQ(10, BUS::GetCommandByteCount(0x2a)); + EXPECT_EQ(10, BUS::GetCommandByteCount(0x2b)); + EXPECT_EQ(10, BUS::GetCommandByteCount(0x2f)); + EXPECT_EQ(10, BUS::GetCommandByteCount(0x35)); + EXPECT_EQ(10, BUS::GetCommandByteCount(0x37)); + EXPECT_EQ(10, BUS::GetCommandByteCount(0x3e)); + EXPECT_EQ(10, BUS::GetCommandByteCount(0x3f)); + EXPECT_EQ(10, BUS::GetCommandByteCount(0x43)); + EXPECT_EQ(10, BUS::GetCommandByteCount(0x4a)); + EXPECT_EQ(10, BUS::GetCommandByteCount(0x55)); + EXPECT_EQ(10, BUS::GetCommandByteCount(0x5a)); + EXPECT_EQ(16, BUS::GetCommandByteCount(0x88)); + EXPECT_EQ(16, BUS::GetCommandByteCount(0x8a)); + EXPECT_EQ(16, BUS::GetCommandByteCount(0x8f)); + EXPECT_EQ(16, BUS::GetCommandByteCount(0x91)); + EXPECT_EQ(16, BUS::GetCommandByteCount(0x9e)); + EXPECT_EQ(16, BUS::GetCommandByteCount(0x9f)); + EXPECT_EQ(12, BUS::GetCommandByteCount(0xa0)); + EXPECT_EQ(10, BUS::GetCommandByteCount(0xc0)); + EXPECT_EQ(10, BUS::GetCommandByteCount(0xc1)); + EXPECT_EQ(0, BUS::GetCommandByteCount(0x1f)); +} + +TEST(BusTest, GetPhase) +{ + EXPECT_EQ(phase_t::dataout, BUS::GetPhase(0b000)); + EXPECT_EQ(phase_t::datain, BUS::GetPhase(0b001)); + EXPECT_EQ(phase_t::command, BUS::GetPhase(0b010)); + EXPECT_EQ(phase_t::status, BUS::GetPhase(0b011)); + EXPECT_EQ(phase_t::reserved, BUS::GetPhase(0b100)); + EXPECT_EQ(phase_t::reserved, BUS::GetPhase(0b101)); + EXPECT_EQ(phase_t::msgout, BUS::GetPhase(0b110)); + EXPECT_EQ(phase_t::msgin, BUS::GetPhase(0b111)); + + NiceMock bus; + + EXPECT_EQ(phase_t::busfree, bus.GetPhase()); + + ON_CALL(bus, GetSEL()).WillByDefault(Return(true)); + EXPECT_EQ(phase_t::selection, bus.GetPhase()); + + ON_CALL(bus, GetSEL()).WillByDefault(Return(false)); + ON_CALL(bus, GetBSY()).WillByDefault(Return(true)); + + ON_CALL(bus, GetMSG()).WillByDefault(Return(false)); + EXPECT_EQ(phase_t::dataout, bus.GetPhase()); + ON_CALL(bus, GetMSG()).WillByDefault(Return(true)); + EXPECT_EQ(phase_t::reserved, bus.GetPhase()); + + ON_CALL(bus, GetMSG()).WillByDefault(Return(false)); + ON_CALL(bus, GetCD()).WillByDefault(Return(true)); + EXPECT_EQ(phase_t::command, bus.GetPhase()); + + ON_CALL(bus, GetMSG()).WillByDefault(Return(true)); + ON_CALL(bus, GetCD()).WillByDefault(Return(true)); + EXPECT_EQ(phase_t::msgout, bus.GetPhase()); + + ON_CALL(bus, GetMSG()).WillByDefault(Return(false)); + ON_CALL(bus, GetCD()).WillByDefault(Return(false)); + ON_CALL(bus, GetIO()).WillByDefault(Return(true)); + EXPECT_EQ(phase_t::datain, bus.GetPhase()); + + ON_CALL(bus, GetMSG()).WillByDefault(Return(true)); + ON_CALL(bus, GetCD()).WillByDefault(Return(false)); + ON_CALL(bus, GetIO()).WillByDefault(Return(true)); + EXPECT_EQ(phase_t::reserved, bus.GetPhase()); + + ON_CALL(bus, GetMSG()).WillByDefault(Return(true)); + ON_CALL(bus, GetCD()).WillByDefault(Return(true)); + ON_CALL(bus, GetIO()).WillByDefault(Return(true)); + EXPECT_EQ(phase_t::msgin, bus.GetPhase()); + + ON_CALL(bus, GetMSG()).WillByDefault(Return(false)); + ON_CALL(bus, GetCD()).WillByDefault(Return(true)); + ON_CALL(bus, GetIO()).WillByDefault(Return(true)); + EXPECT_EQ(phase_t::status, bus.GetPhase()); +} + +TEST(BusTest, GetPhaseName) +{ + EXPECT_EQ("busfree", BUS::GetPhaseName(phase_t::busfree)); + EXPECT_EQ("arbitration", BUS::GetPhaseName(phase_t::arbitration)); + EXPECT_EQ("selection", BUS::GetPhaseName(phase_t::selection)); + EXPECT_EQ("reselection", BUS::GetPhaseName(phase_t::reselection)); + EXPECT_EQ("command", BUS::GetPhaseName(phase_t::command)); + EXPECT_EQ("datain", BUS::GetPhaseName(phase_t::datain)); + EXPECT_EQ("dataout", BUS::GetPhaseName(phase_t::dataout)); + EXPECT_EQ("status", BUS::GetPhaseName(phase_t::status)); + EXPECT_EQ("msgin", BUS::GetPhaseName(phase_t::msgin)); + EXPECT_EQ("msgout", BUS::GetPhaseName(phase_t::msgout)); + EXPECT_EQ("reserved", BUS::GetPhaseName(phase_t::reserved)); +} diff --git a/cpp/test/cd_dvd_test.cpp b/cpp/test/cd_dvd_test.cpp new file mode 100644 index 00000000..2711adce --- /dev/null +++ b/cpp/test/cd_dvd_test.cpp @@ -0,0 +1,134 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" +#include "shared/shared_exceptions.h" +#include +#include + +using namespace std; +using namespace filesystem; + +void CdDvdTest_SetUpModePages(map>& pages) +{ + EXPECT_EQ(7, pages.size()) << "Unexpected number of mode pages"; + EXPECT_EQ(12, pages[1].size()); + EXPECT_EQ(24, pages[3].size()); + EXPECT_EQ(24, pages[4].size()); + EXPECT_EQ(12, pages[8].size()); + EXPECT_EQ(8, pages[13].size()); + EXPECT_EQ(16, pages[14].size()); + EXPECT_EQ(30, pages[48].size()); +} + +TEST(CdDvdTest, Inquiry) +{ + TestInquiry::Inquiry(SCCD, device_type::cd_rom, scsi_level::scsi_2, "SCSI2Pi SCSI CD-ROM ", 0x1f, true); + + TestInquiry::Inquiry(SCCD, device_type::cd_rom, scsi_level::scsi_1_ccs, "SCSI2Pi SCSI CD-ROM ", 0x1f, true, "file.is1"); +} + +TEST(CdDvdTest, GetSectorSizes) +{ + MockCdDvd cd(0); + + const auto& sector_sizes = cd.GetSupportedSectorSizes(); + EXPECT_EQ(2, sector_sizes.size()); + + EXPECT_TRUE(sector_sizes.contains(512)); + EXPECT_TRUE(sector_sizes.contains(2048)); +} + +TEST(CdDvdTest, SetUpModePages) +{ + map> pages; + MockCdDvd cd(0); + + // Non changeable + cd.SetUpModePages(pages, 0x3f, false); + CdDvdTest_SetUpModePages(pages); + + // Changeable + pages.clear(); + cd.SetUpModePages(pages, 0x3f, true); + CdDvdTest_SetUpModePages(pages); +} + +TEST(CdDvdTest, Open) +{ + MockCdDvd cd_iso(0); + MockCdDvd cd_cue(0); + MockCdDvd cd_raw(0); + MockCdDvd cd_physical(0); + + EXPECT_THROW(cd_iso.Open(), io_exception) << "Missing filename"; + + path filename = CreateTempFile(2047); + cd_iso.SetFilename(string(filename)); + EXPECT_THROW(cd_iso.Open(), io_exception) << "ISO CD-ROM image file size too small"; + remove(filename); + + filename = CreateTempFile(2* 2048); + cd_iso.SetFilename(string(filename)); + cd_iso.Open(); + EXPECT_EQ(2, cd_iso.GetBlockCount()); + remove(filename); + + filename = CreateTempFile(0); + ofstream out; + out.open(filename); + array cue = { 'F', 'I', 'L', 'E' }; + out.write(cue.data(), cue.size()); + out.close(); + resize_file(filename, 2 * 2048); + cd_cue.SetFilename(string(filename)); + EXPECT_THROW(cd_cue.Open(), io_exception) << "CUE CD-ROM files are not supported"; + + filename = CreateTempFile(0); + out.open(filename); + array header; + header.fill(0xff); + header[0] = 0; + header[11] = 0; + out.write(header.data(), header.size()); + out.close(); + resize_file(filename, 2 * 2535); + cd_raw.SetFilename(string(filename)); + EXPECT_THROW(cd_raw.Open(), io_exception) << "Illegal raw ISO CD-ROM header"; + header[15] = 0x01; + filename = CreateTempFile(0); + out.open(filename); + out.write(header.data(), header.size()); + out.close(); + cd_raw.SetFilename(string(filename)); + resize_file(filename, 2 * 2536); + cd_raw.Open(); + EXPECT_EQ(2, cd_raw.GetBlockCount()); + remove(filename); + + filename = CreateTempFile(2* 2048); + cd_physical.SetFilename("\\" + string(filename)); + // The respective code in SCSICD appears to be broken, see https://github.com/akuker/PISCSI/issues/919 + EXPECT_THROW(cd_physical.Open(), io_exception) << "Invalid physical CD-ROM file"; + remove(filename); +} + +TEST(CdDvdTest, ReadToc) +{ + auto controller = make_shared(); + auto cd = make_shared(0); + EXPECT_TRUE(cd->Init({})); + + controller->AddDevice(cd); + + EXPECT_THAT([&] { cd->Dispatch(scsi_command::cmd_read_toc); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::not_ready), + Property(&scsi_exception::get_asc, asc::medium_not_present)))); + + // Further testing requires filesystem access +} diff --git a/cpp/test/command_context_test.cpp b/cpp/test/command_context_test.cpp new file mode 100644 index 00000000..b0b5e5b3 --- /dev/null +++ b/cpp/test/command_context_test.cpp @@ -0,0 +1,150 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include + +#include "test/test_shared.h" +#include "shared/shared_exceptions.h" +#include "shared/protobuf_util.h" +#include "s2p/command_context.h" +#include +#include + +using namespace protobuf_util; + +TEST(CommandContext, SetGetDefaultFolder) +{ + PbCommand command; + CommandContext context(command, "folder1", ""); + + EXPECT_EQ("folder1", context.GetDefaultFolder()); + context.SetDefaultFolder("folder2"); + EXPECT_EQ("folder2", context.GetDefaultFolder()); +} + +TEST(CommandContext, ReadCommand) +{ + int fd = open(CreateTempFile(0).string().c_str(), O_RDONLY); + CommandContext context1(fd); + EXPECT_FALSE(context1.ReadCommand()); + close(fd); + + // Invalid magic with wrong length + vector data = { byte{'1'}, byte{'2'}, byte{'3'} }; + fd = open(CreateTempFileWithData(data).string().c_str(), O_RDONLY); + CommandContext context2(fd); + EXPECT_THROW(context2.ReadCommand(), io_exception); + close(fd); + + // Invalid magic with right length + data = { byte{'1'}, byte{'2'}, byte{'3'}, byte{'4'}, byte{'5'}, byte{'6'} }; + fd = open(CreateTempFileWithData(data).string().c_str(), O_RDONLY); + CommandContext context3(fd); + EXPECT_THROW(context3.ReadCommand(), io_exception); + close(fd); + + data = { byte{'R'}, byte{'A'}, byte{'S'}, byte{'C'}, byte{'S'}, byte{'I'}, byte{'1'} }; + // Valid magic but invalid command + fd = open(CreateTempFileWithData(data).string().c_str(), O_RDONLY); + CommandContext context4(fd); + EXPECT_THROW(context4.ReadCommand(), io_exception); + close(fd); + + data = { byte{'R'}, byte{'A'}, byte{'S'}, byte{'C'}, byte{'S'}, byte{'I'} }; + // Valid magic but missing command + fd = open(CreateTempFileWithData(data).string().c_str(), O_RDONLY); + CommandContext context5(fd); + EXPECT_THROW(context5.ReadCommand(), io_exception); + close(fd); + + const string filename = CreateTempFileWithData(data).string(); + fd = open(filename.c_str(), O_RDWR | O_APPEND); + PbCommand command; + command.set_operation(PbOperation::SERVER_INFO); + SerializeMessage(fd, command); + close(fd); + fd = open(filename.c_str(), O_RDONLY); + CommandContext context6(fd); + EXPECT_TRUE(context6.ReadCommand()); + close(fd); + EXPECT_EQ(PbOperation::SERVER_INFO, context6.GetCommand().operation()); +} + +TEST(CommandContext, GetCommand) +{ + PbCommand command; + command.set_operation(PbOperation::SERVER_INFO); + CommandContext context(command, "", ""); + EXPECT_EQ(PbOperation::SERVER_INFO, context.GetCommand().operation()); +} + +TEST(CommandContext, WriteResult) +{ + const string filename = CreateTempFile(0); + int fd = open(filename.c_str(), O_RDWR | O_APPEND); + PbResult result; + result.set_status(false); + result.set_error_code(PbErrorCode::UNAUTHORIZED); + CommandContext context(fd); + context.WriteResult(result); + close(fd); + EXPECT_FALSE(result.status()); + + fd = open(filename.c_str(), O_RDONLY); + result.set_status(true); + DeserializeMessage(fd, result); + close(fd); + EXPECT_FALSE(result.status()); + EXPECT_EQ(PbErrorCode::UNAUTHORIZED, result.error_code()); +} + +TEST(CommandContext, WriteSuccessResult) +{ + const string filename = CreateTempFile(0); + int fd = open(filename.c_str(), O_RDWR | O_APPEND); + PbResult result; + result.set_status(false); + CommandContext context(fd); + context.WriteSuccessResult(result); + close(fd); + EXPECT_TRUE(result.status()); +} + +TEST(CommandContext, ReturnLocalizedError) +{ + PbCommand command; + CommandContext context(command, "", "en_US"); + + EXPECT_FALSE(context.ReturnLocalizedError(LocalizationKey::ERROR_LOG_LEVEL)); +} + +TEST(CommandContext, ReturnSuccessStatus) +{ + PbCommand command; + + CommandContext context1(command, "", ""); + EXPECT_TRUE(context1.ReturnSuccessStatus()); + + const int fd = open("/dev/null", O_RDWR); + CommandContext context2(fd); + EXPECT_TRUE(context2.ReturnSuccessStatus()); + close(fd); +} + +TEST(CommandContext, ReturnErrorStatus) +{ + PbCommand command; + + CommandContext context1(command, "", ""); + EXPECT_FALSE(context1.ReturnErrorStatus("error")); + + const int fd = open("/dev/null", O_RDWR); + CommandContext context2(fd); + EXPECT_FALSE(context2.ReturnErrorStatus("error")); + close(fd); +} diff --git a/cpp/test/controller_manager_test.cpp b/cpp/test/controller_manager_test.cpp new file mode 100644 index 00000000..6efba8e4 --- /dev/null +++ b/cpp/test/controller_manager_test.cpp @@ -0,0 +1,86 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" +#include "base/device_factory.h" +#include "controllers/controller_factory.h" + +TEST(ControllerFactoryTest, LifeCycle) +{ + const int ID1 = 4; + const int ID2 = 5; + const int LUN1 = 0; + const int LUN2 = 3; + + auto bus = make_shared(); + ControllerFactory controller_factory; + DeviceFactory device_factory; + + auto device = device_factory.CreateDevice(SCHS, -1, ""); + EXPECT_FALSE(controller_factory.AttachToController(*bus, ID1, device)); + + device = device_factory.CreateDevice(SCHS, LUN1, ""); + EXPECT_TRUE(controller_factory.AttachToController(*bus, ID1, device)); + EXPECT_TRUE(controller_factory.HasController(ID1)); + auto controller = controller_factory.FindController(ID1); + EXPECT_NE(nullptr, controller); + EXPECT_EQ(1, controller->GetLunCount()); + EXPECT_FALSE(controller_factory.HasController(0)); + EXPECT_EQ(nullptr, controller_factory.FindController(0)); + EXPECT_TRUE(controller_factory.HasDeviceForIdAndLun(ID1, LUN1)); + EXPECT_NE(nullptr, controller_factory.GetDeviceForIdAndLun(ID1, LUN1)); + EXPECT_FALSE(controller_factory.HasDeviceForIdAndLun(0, 0)); + EXPECT_EQ(nullptr, controller_factory.GetDeviceForIdAndLun(0, 0)); + + device = device_factory.CreateDevice(SCHS, LUN2, ""); + EXPECT_TRUE(controller_factory.AttachToController(*bus, ID1, device)); + EXPECT_TRUE(controller_factory.HasController(ID1)); + controller = controller_factory.FindController(ID1); + EXPECT_NE(nullptr, controller_factory.FindController(ID1)); + EXPECT_TRUE(controller_factory.DeleteController(*controller)); + EXPECT_EQ(nullptr, controller_factory.FindController(ID1)); + + auto disk = make_shared(); + EXPECT_TRUE(controller_factory.AttachToController(*bus, ID2, disk)); + EXPECT_CALL(*disk, FlushCache); + controller_factory.DeleteAllControllers(); + EXPECT_FALSE(controller_factory.HasController(ID1)); + EXPECT_EQ(nullptr, controller_factory.FindController(ID1)); + EXPECT_EQ(nullptr, controller_factory.GetDeviceForIdAndLun(ID1, LUN1)); + EXPECT_FALSE(controller_factory.HasDeviceForIdAndLun(ID1, LUN1)); + EXPECT_FALSE(controller_factory.HasController(ID2)); + EXPECT_EQ(nullptr, controller_factory.FindController(ID2)); + EXPECT_EQ(nullptr, controller_factory.GetDeviceForIdAndLun(ID2, LUN1)); + EXPECT_FALSE(controller_factory.HasDeviceForIdAndLun(ID2, LUN1)); +} + +TEST(ControllerFactoryTest, AttachToController) +{ + const int ID = 4; + const int LUN1 = 3; + const int LUN2 = 0; + + auto bus = make_shared(); + ControllerFactory controller_factory; + DeviceFactory device_factory; + + auto device1 = device_factory.CreateDevice(SCHS, LUN1, ""); + EXPECT_FALSE(controller_factory.AttachToController(*bus, ID, device1)) << "LUN 0 is missing"; + + auto device2 = device_factory.CreateDevice(SCLP, LUN2, ""); + EXPECT_TRUE(controller_factory.AttachToController(*bus, ID, device2)); + EXPECT_TRUE(controller_factory.AttachToController(*bus, ID, device1)); + EXPECT_FALSE(controller_factory.AttachToController(*bus, ID, device1)); +} + +TEST(ControllerFactory, ProcessOnController) +{ + ControllerFactory controller_factory; + + EXPECT_EQ(AbstractController::shutdown_mode::NONE, controller_factory.ProcessOnController(0)); +} diff --git a/cpp/test/ctapdriver_test.cpp b/cpp/test/ctapdriver_test.cpp new file mode 100644 index 00000000..e6f23045 --- /dev/null +++ b/cpp/test/ctapdriver_test.cpp @@ -0,0 +1,41 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include + +#include "devices/ctapdriver.h" + +TEST(CTapDriverTest, Crc32) +{ + array buf; + + buf.fill(0x00); + EXPECT_EQ(0xe3d887bb, CTapDriver::Crc32(span(buf.data(), ETH_FRAME_LEN))); + + buf.fill(0xff); + EXPECT_EQ(0x814765f4, CTapDriver::Crc32(span(buf.data(), ETH_FRAME_LEN))); + + buf.fill(0x10); + EXPECT_EQ(0xb7288Cd3, CTapDriver::Crc32(span(buf.data(), ETH_FRAME_LEN))); + + buf.fill(0x7f); + EXPECT_EQ(0x4b543477, CTapDriver::Crc32(span(buf.data(), ETH_FRAME_LEN))); + + buf.fill(0x80); + EXPECT_EQ(0x29cbd638, CTapDriver::Crc32(span(buf.data(), ETH_FRAME_LEN))); + + for (size_t i = 0; i < buf.size(); i++) { + buf[i] = (uint8_t)i; + } + EXPECT_EQ(0xe7870705, CTapDriver::Crc32(span(buf.data(), ETH_FRAME_LEN))); + + for (size_t i = buf.size() - 1; i > 0; i--) { + buf[i] = (uint8_t)i; + } + EXPECT_EQ(0xe7870705, CTapDriver::Crc32(span(buf.data(), ETH_FRAME_LEN))); +} diff --git a/cpp/test/daynaport_test.cpp b/cpp/test/daynaport_test.cpp new file mode 100644 index 00000000..31702fc6 --- /dev/null +++ b/cpp/test/daynaport_test.cpp @@ -0,0 +1,186 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" +#include "shared/shared_exceptions.h" +#include "devices/daynaport.h" + +TEST(ScsiDaynaportTest, GetDefaultParams) +{ + const auto [controller, daynaport] = CreateDevice(SCDP); + const auto params = daynaport->GetDefaultParams(); + EXPECT_EQ(2, params.size()); +} + +TEST(ScsiDaynaportTest, Inquiry) +{ + TestInquiry::Inquiry(SCDP, device_type::processor, scsi_level::scsi_2, "Dayna SCSI/Link 1.4a", 0x20, false); +} + +TEST(ScsiDaynaportTest, TestUnitReady) +{ + auto [controller, daynaport] = CreateDevice(SCDP); + + EXPECT_CALL(*controller, Status()); + daynaport->Dispatch(scsi_command::cmd_test_unit_ready); + EXPECT_EQ(status::good, controller->GetStatus()); +} + +TEST(ScsiDaynaportTest, Read) +{ + auto [controller, daynaport] = CreateDevice(SCDP); + + // ALLOCATION LENGTH + controller->SetCmdByte(4, 1); + vector buf(0); + EXPECT_EQ(0, dynamic_pointer_cast(daynaport)->Read(controller->GetCmd(), buf, 0)) << "Trying to read the root sector must fail"; +} + +TEST(ScsiDaynaportTest, Write) +{ + auto [controller, daynaport] = CreateDevice(SCDP); + + // Unknown data format + controller->SetCmdByte(5, 0xff); + vector buf(0); + EXPECT_TRUE(dynamic_pointer_cast(daynaport)->Write(controller->GetCmd(), buf)); +} + +TEST(ScsiDaynaportTest, Read6) +{ + auto [controller, daynaport] = CreateDevice(SCDP); + // Required by the bullseye clang++ compiler + auto d = daynaport; + + controller->SetCmdByte(5, 0xff); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_read6); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "Invalid data format"; +} + +TEST(ScsiDaynaportTest, Write6) +{ + auto [controller, daynaport] = CreateDevice(SCDP); + // Required by the bullseye clang++ compiler + auto d = daynaport; + + controller->SetCmdByte(5, 0x00); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_write6); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "Invalid transfer length"; + + controller->SetCmdByte(3, -1); + controller->SetCmdByte(4, -8); + controller->SetCmdByte(5, 0x08); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_write6); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "Invalid transfer length"; + + controller->SetCmdByte(3, 0); + controller->SetCmdByte(4, 0); + controller->SetCmdByte(5, 0xff); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_write6); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "Invalid transfer length"; +} + +TEST(ScsiDaynaportTest, TestRetrieveStats) +{ + auto [controller, daynaport] = CreateDevice(SCDP); + + // ALLOCATION LENGTH + controller->SetCmdByte(4, 255); + EXPECT_CALL(*controller, DataIn()); + daynaport->Dispatch(scsi_command::cmd_retrieve_stats); +} + +TEST(ScsiDaynaportTest, SetInterfaceMode) +{ + auto [controller, daynaport] = CreateDevice(SCDP); + // Required by the bullseye clang++ compiler + auto d = daynaport; + + // Unknown interface command + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_set_iface_mode); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_command_operation_code)))); + + // Not implemented, do nothing + controller->SetCmdByte(5, SCSIDaynaPort::CMD_SCSILINK_SETMODE); + EXPECT_CALL(*controller, Status()); + daynaport->Dispatch(scsi_command::cmd_set_iface_mode); + EXPECT_EQ(status::good, controller->GetStatus()); + + controller->SetCmdByte(5, SCSIDaynaPort::CMD_SCSILINK_SETMAC); + EXPECT_CALL(*controller, DataOut()); + daynaport->Dispatch(scsi_command::cmd_set_iface_mode); + + // Not implemented + controller->SetCmdByte(5, SCSIDaynaPort::CMD_SCSILINK_STATS); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_set_iface_mode); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_command_operation_code)))); + + // Not implemented + controller->SetCmdByte(5, SCSIDaynaPort::CMD_SCSILINK_ENABLE); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_set_iface_mode); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_command_operation_code)))); + + // Not implemented + controller->SetCmdByte(5, SCSIDaynaPort::CMD_SCSILINK_SET); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_set_iface_mode); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_command_operation_code)))); +} + +TEST(ScsiDaynaportTest, SetMcastAddr) +{ + auto [controller, daynaport] = CreateDevice(SCDP); + // Required by the bullseye clang++ compiler + auto d = daynaport; + + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_set_mcast_addr); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "Length of 0 is not supported"; + + controller->SetCmdByte(4, 1); + EXPECT_CALL(*controller, DataOut()); + daynaport->Dispatch(scsi_command::cmd_set_mcast_addr); +} + +TEST(ScsiDaynaportTest, EnableInterface) +{ + auto [controller, daynaport] = CreateDevice(SCDP); + // Required by the bullseye clang++ compiler + auto d = daynaport; + + // Enable + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_enable_interface); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::aborted_command), + Property(&scsi_exception::get_asc, asc::no_additional_sense_information)))); + + // Disable + controller->SetCmdByte(5, 0x00); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_enable_interface); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::aborted_command), + Property(&scsi_exception::get_asc, asc::no_additional_sense_information)))); +} + +TEST(ScsiDaynaportTest, GetSendDelay) +{ + SCSIDaynaPort daynaport(0); + daynaport.Init({}); + + EXPECT_EQ(6, daynaport.GetSendDelay()); +} diff --git a/cpp/test/device_factory_test.cpp b/cpp/test/device_factory_test.cpp new file mode 100644 index 00000000..3870bb2a --- /dev/null +++ b/cpp/test/device_factory_test.cpp @@ -0,0 +1,221 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include +#include "mocks.h" +#include "shared/shared_exceptions.h" +#include "shared/s2p_version.h" +#include "controllers/controller_factory.h" +#include "base/device_factory.h" + +string GetVersion() +{ + return fmt::format("{0:02}{1:02}", s2p_major_version, s2p_minor_version); +} + +TEST(DeviceFactoryTest, GetTypeForFile) +{ + DeviceFactory device_factory; + + EXPECT_EQ(device_factory.GetTypeForFile("test.hd1"), SCHD); + EXPECT_EQ(device_factory.GetTypeForFile("test.hds"), SCHD); + EXPECT_EQ(device_factory.GetTypeForFile("test.HDS"), SCHD); + EXPECT_EQ(device_factory.GetTypeForFile("test.hda"), SCHD); + EXPECT_EQ(device_factory.GetTypeForFile("test.hdr"), SCRM); + EXPECT_EQ(device_factory.GetTypeForFile("test.mos"), SCMO); + EXPECT_EQ(device_factory.GetTypeForFile("test.iso"), SCCD); + EXPECT_EQ(device_factory.GetTypeForFile("test.is1"), SCCD); + EXPECT_EQ(device_factory.GetTypeForFile("test.suffix.iso"), SCCD); + EXPECT_EQ(device_factory.GetTypeForFile("daynaport"), SCDP); + EXPECT_EQ(device_factory.GetTypeForFile("printer"), SCLP); + EXPECT_EQ(device_factory.GetTypeForFile("services"), SCHS); + EXPECT_EQ(device_factory.GetTypeForFile("unknown"), UNDEFINED); + EXPECT_EQ(device_factory.GetTypeForFile("test.iso.suffix"), UNDEFINED); +} + +TEST(DeviceFactoryTest, GetExtensionMapping) +{ + DeviceFactory device_factory; + + auto mapping = device_factory.GetExtensionMapping(); + EXPECT_EQ(7, mapping.size()); + EXPECT_EQ(SCHD, mapping["hd1"]); + EXPECT_EQ(SCHD, mapping["hds"]); + EXPECT_EQ(SCHD, mapping["hda"]); + EXPECT_EQ(SCRM, mapping["hdr"]); + EXPECT_EQ(SCMO, mapping["mos"]); + EXPECT_EQ(SCCD, mapping["iso"]); + EXPECT_EQ(SCCD, mapping["is1"]); +} + +TEST(DeviceFactoryTest, UnknownDeviceType) +{ + DeviceFactory device_factory; + + auto device1 = device_factory.CreateDevice(UNDEFINED, 0, "test"); + EXPECT_EQ(nullptr, device1); +} + +TEST(DeviceFactoryTest, SCHD_Device_Defaults) +{ + DeviceFactory device_factory; + + auto device = device_factory.CreateDevice(UNDEFINED, 0, "test.hda"); + + EXPECT_NE(nullptr, device); + EXPECT_EQ(SCHD, device->GetType()); + EXPECT_TRUE(device->SupportsFile()); + EXPECT_FALSE(device->SupportsParams()); + EXPECT_TRUE(device->IsProtectable()); + EXPECT_FALSE(device->IsProtected()); + EXPECT_FALSE(device->IsReadOnly()); + EXPECT_FALSE(device->IsRemovable()); + EXPECT_FALSE(device->IsRemoved()); + EXPECT_FALSE(device->IsLockable()); + EXPECT_FALSE(device->IsLocked()); + EXPECT_TRUE(device->IsStoppable()); + EXPECT_FALSE(device->IsStopped()); + + EXPECT_EQ("QUANTUM", device->GetVendor()) << "Invalid default vendor for Apple drive"; + EXPECT_EQ("FIREBALL", device->GetProduct()) << "Invalid default vendor for Apple drive"; + EXPECT_EQ(GetVersion(), device->GetRevision()); + + device = device_factory.CreateDevice(UNDEFINED, 0, "test.hds"); + EXPECT_NE(nullptr, device); + EXPECT_EQ(SCHD, device->GetType()); +} + +void TestRemovableDrive(PbDeviceType type, const string& filename, const string& product) +{ + DeviceFactory device_factory; + auto device = device_factory.CreateDevice(UNDEFINED, 0, filename); + + EXPECT_NE(nullptr, device); + EXPECT_EQ(type, device->GetType()); + EXPECT_TRUE(device->SupportsFile()); + EXPECT_FALSE(device->SupportsParams()); + EXPECT_TRUE(device->IsProtectable()); + EXPECT_FALSE(device->IsProtected()); + EXPECT_FALSE(device->IsReadOnly()); + EXPECT_TRUE(device->IsRemovable()); + EXPECT_FALSE(device->IsRemoved()); + EXPECT_TRUE(device->IsLockable()); + EXPECT_FALSE(device->IsLocked()); + EXPECT_TRUE(device->IsStoppable()); + EXPECT_FALSE(device->IsStopped()); + + EXPECT_EQ("SCSI2Pi", device->GetVendor()); + EXPECT_EQ(product, device->GetProduct()); + EXPECT_EQ(GetVersion(), device->GetRevision()); +} + +TEST(DeviceFactoryTest, SCRM_Device_Defaults) +{ + TestRemovableDrive(SCRM, "test.hdr", "SCSI HD (REM.)"); +} + +TEST(DeviceFactoryTest, SCMO_Device_Defaults) +{ + TestRemovableDrive(SCMO, "test.mos", "SCSI MO"); +} + +TEST(DeviceFactoryTest, SCCD_Device_Defaults) +{ + DeviceFactory device_factory; + + auto device = device_factory.CreateDevice(UNDEFINED, 0, "test.iso"); + EXPECT_NE(nullptr, device); + EXPECT_EQ(SCCD, device->GetType()); + EXPECT_TRUE(device->SupportsFile()); + EXPECT_FALSE(device->SupportsParams()); + EXPECT_FALSE(device->IsProtectable()); + EXPECT_FALSE(device->IsProtected()); + EXPECT_TRUE(device->IsReadOnly()); + EXPECT_TRUE(device->IsRemovable()); + EXPECT_FALSE(device->IsRemoved()); + EXPECT_TRUE(device->IsLockable()); + EXPECT_FALSE(device->IsLocked()); + EXPECT_TRUE(device->IsStoppable()); + EXPECT_FALSE(device->IsStopped()); + + EXPECT_EQ("SCSI2Pi", device->GetVendor()); + EXPECT_EQ("SCSI CD-ROM", device->GetProduct()); + EXPECT_EQ(GetVersion(), device->GetRevision()); +} + +TEST(DeviceFactoryTest, SCDP_Device_Defaults) +{ + DeviceFactory device_factory; + + auto device = device_factory.CreateDevice(UNDEFINED, 0, "daynaport"); + EXPECT_NE(nullptr, device); + EXPECT_EQ(SCDP, device->GetType()); + EXPECT_FALSE(device->SupportsFile()); + EXPECT_TRUE(device->SupportsParams()); + EXPECT_FALSE(device->IsProtectable()); + EXPECT_FALSE(device->IsProtected()); + EXPECT_FALSE(device->IsReadOnly()); + EXPECT_FALSE(device->IsRemovable()); + EXPECT_FALSE(device->IsRemoved()); + EXPECT_FALSE(device->IsLockable()); + EXPECT_FALSE(device->IsLocked()); + EXPECT_FALSE(device->IsStoppable()); + EXPECT_FALSE(device->IsStopped()); + + EXPECT_EQ("Dayna", device->GetVendor()); + EXPECT_EQ("SCSI/Link", device->GetProduct()); + EXPECT_EQ("1.4a", device->GetRevision()); +} + +TEST(DeviceFactoryTest, SCHS_Device_Defaults) +{ + DeviceFactory device_factory; + + auto device = device_factory.CreateDevice(UNDEFINED, 0, "services"); + EXPECT_NE(nullptr, device); + EXPECT_EQ(SCHS, device->GetType()); + EXPECT_FALSE(device->SupportsFile()); + EXPECT_FALSE(device->SupportsParams()); + EXPECT_FALSE(device->IsProtectable()); + EXPECT_FALSE(device->IsProtected()); + EXPECT_FALSE(device->IsReadOnly()); + EXPECT_FALSE(device->IsRemovable()); + EXPECT_FALSE(device->IsRemoved()); + EXPECT_FALSE(device->IsLockable()); + EXPECT_FALSE(device->IsLocked()); + EXPECT_FALSE(device->IsStoppable()); + EXPECT_FALSE(device->IsStopped()); + + EXPECT_EQ("SCSI2Pi", device->GetVendor()); + EXPECT_EQ("Host Services", device->GetProduct()); + EXPECT_EQ(GetVersion(), device->GetRevision()); +} + +TEST(DeviceFactoryTest, SCLP_Device_Defaults) +{ + DeviceFactory device_factory; + + auto device = device_factory.CreateDevice(UNDEFINED, 0, "printer"); + EXPECT_NE(nullptr, device); + EXPECT_EQ(SCLP, device->GetType()); + EXPECT_FALSE(device->SupportsFile()); + EXPECT_TRUE(device->SupportsParams()); + EXPECT_FALSE(device->IsProtectable()); + EXPECT_FALSE(device->IsProtected()); + EXPECT_FALSE(device->IsReadOnly()); + EXPECT_FALSE(device->IsRemovable()); + EXPECT_FALSE(device->IsRemoved()); + EXPECT_FALSE(device->IsLockable()); + EXPECT_FALSE(device->IsLocked()); + EXPECT_FALSE(device->IsStoppable()); + EXPECT_FALSE(device->IsStopped()); + + EXPECT_EQ("SCSI2Pi", device->GetVendor()); + EXPECT_EQ("SCSI PRINTER", device->GetProduct()); + EXPECT_EQ(GetVersion(), device->GetRevision()); +} diff --git a/cpp/test/device_test.cpp b/cpp/test/device_test.cpp new file mode 100644 index 00000000..6ad6f4c8 --- /dev/null +++ b/cpp/test/device_test.cpp @@ -0,0 +1,270 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" +#include "base/device.h" + +TEST(DeviceTest, GetDefaultParams) +{ + MockDevice device(0); + + const auto params = device.GetDefaultParams(); + EXPECT_TRUE(params.empty()); +} + +TEST(DeviceTest, Properties) +{ + const int LUN = 5; + + MockDevice device(LUN); + + EXPECT_FALSE(device.IsReady()) << "Wrong default value"; + device.SetReady(true); + EXPECT_TRUE(device.IsReady()); + device.SetReady(false); + EXPECT_FALSE(device.IsReady()); + + EXPECT_FALSE(device.IsReset()) << "Wrong default value"; + device.SetReset(true); + EXPECT_TRUE(device.IsReset()); + device.SetReset(false); + EXPECT_FALSE(device.IsReset()); + + EXPECT_FALSE(device.IsAttn()) << "Wrong default value"; + device.SetAttn(true); + EXPECT_TRUE(device.IsAttn()); + device.SetAttn(false); + EXPECT_FALSE(device.IsAttn()); + + EXPECT_FALSE(device.IsReadOnly()) << "Wrong default value"; + device.SetReadOnly(true); + EXPECT_TRUE(device.IsReadOnly()); + device.SetReadOnly(false); + EXPECT_FALSE(device.IsReadOnly()); + + EXPECT_FALSE(device.IsProtectable()) << "Wrong default value"; + device.SetProtectable(true); + EXPECT_TRUE(device.IsProtectable()); + device.SetProtectable(false); + EXPECT_FALSE(device.IsProtectable()); + + EXPECT_FALSE(device.IsProtected()) << "Wrong default value"; + device.SetProtected(true); + EXPECT_FALSE(device.IsProtected()); + device.SetProtectable(true); + device.SetProtected(true); + EXPECT_TRUE(device.IsProtected()); + device.SetProtected(false); + EXPECT_FALSE(device.IsProtected()); + + device.SetProtectable(false); + device.SetReadOnly(true); + device.SetProtected(true); + EXPECT_FALSE(device.IsProtected()) << "Read-only or not protectable devices cannot be protected"; + device.SetReadOnly(false); + device.SetProtected(true); + EXPECT_FALSE(device.IsProtected()) << "Read-only or not protectable devices cannot be protected"; + + EXPECT_FALSE(device.IsStoppable()) << "Wrong default value"; + device.SetStoppable(true); + EXPECT_TRUE(device.IsStoppable()); + device.SetStoppable(false); + EXPECT_FALSE(device.IsStoppable()); + + EXPECT_FALSE(device.IsStopped()) << "Wrong default value"; + device.SetStopped(true); + EXPECT_TRUE(device.IsStopped()); + device.SetStopped(false); + EXPECT_FALSE(device.IsStopped()); + + EXPECT_FALSE(device.IsRemovable()) << "Wrong default value"; + device.SetRemovable(true); + EXPECT_TRUE(device.IsRemovable()); + device.SetRemovable(false); + EXPECT_FALSE(device.IsRemovable()); + + EXPECT_FALSE(device.IsRemoved()) << "Wrong default value"; + device.SetRemoved(true); + EXPECT_TRUE(device.IsRemoved()); + device.SetRemoved(false); + EXPECT_FALSE(device.IsRemoved()); + + EXPECT_FALSE(device.IsLockable()) << "Wrong default value"; + device.SetLockable(true); + EXPECT_TRUE(device.IsLockable()); + device.SetLockable(false); + EXPECT_FALSE(device.IsLockable()); + + EXPECT_FALSE(device.IsLocked()) << "Wrong default value"; + device.SetLocked(true); + EXPECT_TRUE(device.IsLocked()); + device.SetLocked(false); + EXPECT_FALSE(device.IsLocked()); + + EXPECT_FALSE(device.SupportsParams()) << "Wrong default value"; + device.SupportsParams(true); + EXPECT_TRUE(device.SupportsParams()); + device.SupportsParams(false); + EXPECT_FALSE(device.SupportsParams()); + + EXPECT_FALSE(device.SupportsFile()) << "Wrong default value"; + device.SupportsFile(true); + EXPECT_TRUE(device.SupportsFile()); + device.SupportsFile(false); + EXPECT_FALSE(device.SupportsFile()); + + EXPECT_EQ(LUN, device.GetLun()); +} + +TEST(DeviceTest, GetTypeString) +{ + MockDevice schd(SCHD); + EXPECT_EQ("SCHD", schd.GetTypeString()); + + MockDevice scrm(SCRM); + EXPECT_EQ("SCRM", scrm.GetTypeString()); + + MockDevice scmo(SCMO); + EXPECT_EQ("SCMO", scmo.GetTypeString()); + + MockDevice sccd(SCCD); + EXPECT_EQ("SCCD", sccd.GetTypeString()); + + MockDevice schs(SCHS); + EXPECT_EQ("SCHS", schs.GetTypeString()); + + MockDevice scdp(SCDP); + EXPECT_EQ("SCDP", scdp.GetTypeString()); + + MockDevice sclp(SCLP); + EXPECT_EQ("SCLP", sclp.GetTypeString()); +} + +TEST(DeviceTest, GetIdentifier) +{ + MockDevice device(1); + + EXPECT_CALL(device, GetId()); + EXPECT_EQ("UNDEFINED 0:1", device.GetIdentifier()); +} + +TEST(DeviceTest, Vendor) +{ + MockDevice device(0); + + EXPECT_THROW(device.SetVendor(""), invalid_argument); + EXPECT_THROW(device.SetVendor("123456789"), invalid_argument); + device.SetVendor("12345678"); + EXPECT_EQ("12345678", device.GetVendor()); +} + +TEST(DeviceTest, Product) +{ + MockDevice device(0); + + EXPECT_THROW(device.SetProduct(""), invalid_argument); + EXPECT_THROW(device.SetProduct("12345678901234567"), invalid_argument); + device.SetProduct("1234567890123456"); + EXPECT_EQ("1234567890123456", device.GetProduct()); + device.SetProduct("xyz", false); + EXPECT_EQ("1234567890123456", device.GetProduct()) << "Changing vital product data is not SCSI compliant"; +} + +TEST(DeviceTest, Revision) +{ + MockDevice device(0); + + EXPECT_THROW(device.SetRevision(""), invalid_argument); + EXPECT_THROW(device.SetRevision("12345"), invalid_argument); + device.SetRevision("1234"); + EXPECT_EQ("1234", device.GetRevision()); +} + +TEST(DeviceTest, GetPaddedName) +{ + MockDevice device(0); + + device.SetVendor("V"); + device.SetProduct("P"); + device.SetRevision("R"); + + EXPECT_EQ("V P R ", device.GetPaddedName()); +} + +TEST(DeviceTest, StatusCode) +{ + MockDevice device(0); + + device.SetStatusCode(123); + EXPECT_EQ(123, device.GetStatusCode()); +} + +TEST(DeviceTest, Reset) +{ + MockDevice device(0); + + device.SetLocked(true); + device.SetAttn(true); + device.SetReset(true); + device.Reset(); + EXPECT_FALSE(device.IsLocked()); + EXPECT_FALSE(device.IsAttn()); + EXPECT_FALSE(device.IsReset()); +} + +TEST(DeviceTest, Start) +{ + MockDevice device(0); + + device.SetStopped(true); + device.SetReady(false); + EXPECT_FALSE(device.Start()); + EXPECT_TRUE(device.IsStopped()); + device.SetReady(true); + EXPECT_TRUE(device.Start()); + EXPECT_FALSE(device.IsStopped()); +} + +TEST(DeviceTest, Stop) +{ + MockDevice device(0); + + device.SetReady(true); + device.SetAttn(true); + device.SetStopped(false); + device.Stop(); + EXPECT_FALSE(device.IsReady()); + EXPECT_FALSE(device.IsAttn()); + EXPECT_TRUE(device.IsStopped()); +} + +TEST(DeviceTest, Eject) +{ + MockDevice device(0); + + device.SetReady(false); + device.SetRemovable(false); + EXPECT_FALSE(device.Eject(false)); + + device.SetReady(true); + EXPECT_FALSE(device.Eject(false)); + + device.SetRemovable(true); + device.SetLocked(true); + EXPECT_FALSE(device.Eject(false)); + EXPECT_TRUE(device.Eject(true)); + + device.SetReady(true); + device.SetLocked(false); + EXPECT_TRUE(device.Eject(false)); + EXPECT_FALSE(device.IsReady()); + EXPECT_FALSE(device.IsAttn()); + EXPECT_TRUE(device.IsRemoved()); + EXPECT_FALSE(device.IsLocked()); + EXPECT_TRUE(device.IsStopped()); +} diff --git a/cpp/test/disk_test.cpp b/cpp/test/disk_test.cpp new file mode 100644 index 00000000..b177a46c --- /dev/null +++ b/cpp/test/disk_test.cpp @@ -0,0 +1,825 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" +#include "shared/shared_exceptions.h" +#include "devices/disk.h" +#include "base/scsi_command_util.h" + +using namespace scsi_defs; +using namespace scsi_command_util; + +pair, shared_ptr> CreateDisk() +{ + auto controller = make_shared>(0); + auto disk = make_shared(); + EXPECT_TRUE(disk->Init({})); + EXPECT_TRUE(controller->AddDevice(disk)); + + return { controller, disk }; +} + +TEST(DiskTest, Dispatch) +{ + auto [controller, disk] = CreateDisk(); + + disk->SetRemovable(true); + disk->SetMediumChanged(false); + disk->SetReady(true); + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_test_unit_ready); + EXPECT_EQ(status::good, controller->GetStatus()); + + disk->SetMediumChanged(true); + EXPECT_CALL(*controller, Error); + disk->Dispatch(scsi_command::cmd_test_unit_ready); + EXPECT_FALSE(disk->IsMediumChanged()); +} + +TEST(DiskTest, Rezero) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_rezero); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::not_ready), + Property(&scsi_exception::get_asc, asc::medium_not_present)))) + << "REZERO must fail because drive is not ready"; + + disk->SetReady(true); + + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_rezero); + EXPECT_EQ(status::good, controller->GetStatus()); +} + +TEST(DiskTest, FormatUnit) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_format_unit); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::not_ready), + Property(&scsi_exception::get_asc, asc::medium_not_present)))) + << "FORMAT UNIT must fail because drive is not ready"; + + disk->SetReady(true); + + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_format_unit); + EXPECT_EQ(status::good, controller->GetStatus()); + + controller->SetCmdByte(1, 0x10); + controller->SetCmdByte(4, 1); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_format_unit); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))); +} + +TEST(DiskTest, ReassignBlocks) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_reassign_blocks); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::not_ready), + Property(&scsi_exception::get_asc, asc::medium_not_present)))) + << "REASSIGN must fail because drive is not ready"; + + disk->SetReady(true); + + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_reassign_blocks); + EXPECT_EQ(status::good, controller->GetStatus()); +} + +TEST(DiskTest, Seek6) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_seek6); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::lba_out_of_range)))) + << "SEEK(6) must fail for a medium with 0 blocks"; + + disk->SetBlockCount(1); + // Block count + controller->SetCmdByte(4, 1); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_seek6); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::not_ready), + Property(&scsi_exception::get_asc, asc::medium_not_present)))) + << "SEEK(6) must fail because drive is not ready"; + + disk->SetReady(true); + + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_seek6); + EXPECT_EQ(status::good, controller->GetStatus()); +} + +TEST(DiskTest, Seek10) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_seek10); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::lba_out_of_range)))) + << "SEEK(10) must fail for a medium with 0 blocks"; + + disk->SetBlockCount(1); + // Block count + controller->SetCmdByte(5, 1); + + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_seek10); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::not_ready), + Property(&scsi_exception::get_asc, asc::medium_not_present)))) + << "SEEK(10) must fail because drive is not ready"; + + disk->SetReady(true); + + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_seek10); + EXPECT_EQ(status::good, controller->GetStatus()); +} + +TEST(DiskTest, ReadCapacity10) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_read_capacity10); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::not_ready), + Property(&scsi_exception::get_asc, asc::medium_not_present)))) + << "READ CAPACITY(10) must fail because drive is not ready"; + + disk->SetReady(true); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_read_capacity10); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::medium_not_present)))) + << "READ CAPACITY(10) must fail because the medium has no capacity"; + + disk->SetBlockCount(0x12345678); + EXPECT_CALL(*controller, DataIn); + disk->Dispatch(scsi_command::cmd_read_capacity10); + auto& buf = controller->GetBuffer(); + EXPECT_EQ(0x1234, GetInt16(buf, 0)); + EXPECT_EQ(0x5677, GetInt16(buf, 2)); + + disk->SetBlockCount(0x1234567887654321); + EXPECT_CALL(*controller, DataIn); + disk->Dispatch(scsi_command::cmd_read_capacity10); + buf = controller->GetBuffer(); + EXPECT_EQ(0xffff, GetInt16(buf, 0)); + EXPECT_EQ(0xffff, GetInt16(buf, 2)); +} + +TEST(DiskTest, ReadCapacity16) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + controller->SetCmdByte(1, 0x00); + + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_read_capacity16_read_long16); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "Neither READ CAPACITY(16) nor READ LONG(16)"; + + // READ CAPACITY(16), not READ LONG(16) + controller->SetCmdByte(1, 0x10); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_read_capacity16_read_long16); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::not_ready), + Property(&scsi_exception::get_asc, asc::medium_not_present)))) + << "READ CAPACITY(16) must fail because drive is not ready"; + + disk->SetReady(true); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_read_capacity16_read_long16); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::medium_not_present)))) + << "READ CAPACITY(16) must fail because the medium has no capacity"; + + disk->SetBlockCount(0x1234567887654321); + disk->SetSectorSizeInBytes(1024); + EXPECT_CALL(*controller, DataIn); + disk->Dispatch(scsi_command::cmd_read_capacity16_read_long16); + const auto& buf = controller->GetBuffer(); + EXPECT_EQ(0x1234, GetInt16(buf, 0)); + EXPECT_EQ(0x5678, GetInt16(buf, 2)); + EXPECT_EQ(0x8765, GetInt16(buf, 4)); + EXPECT_EQ(0x4320, GetInt16(buf, 6)); + EXPECT_EQ(0x0000, GetInt16(buf, 8)); + EXPECT_EQ(0x0400, GetInt16(buf, 10)); +} + +TEST(DiskTest, Read6) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_read6); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::lba_out_of_range)))) + << "READ(6) must fail for a medium with 0 blocks"; + + // Further testing requires filesystem access +} + +TEST(DiskTest, Read10) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_read10); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::lba_out_of_range)))) + << "READ(10) must fail for a medium with 0 blocks"; + + disk->SetBlockCount(1); + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_read10); + EXPECT_EQ(status::good, controller->GetStatus()); + + // Further testing requires filesystem access +} + +TEST(DiskTest, Read16) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_read16); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::lba_out_of_range)))) + << "READ(16) must fail for a medium with 0 blocks"; + + disk->SetBlockCount(1); + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_read16); + EXPECT_EQ(status::good, controller->GetStatus()); + + // Further testing requires filesystem access +} + +TEST(DiskTest, Write6) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_write6); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::lba_out_of_range)))) + << "WRITE(6) must fail for a medium with 0 blocks"; + + disk->SetBlockCount(1); + disk->SetReady(true); + disk->SetProtectable(true); + disk->SetProtected(true); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_write6); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::data_protect), + Property(&scsi_exception::get_asc, asc::write_protected)))); + + // Further testing requires filesystem access +} + +TEST(DiskTest, Write10) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_write10); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::lba_out_of_range)))) + << "WRITE(10) must fail for a medium with 0 blocks"; + + disk->SetBlockCount(1); + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_write10); + EXPECT_EQ(status::good, controller->GetStatus()); + + // Further testing requires filesystem access +} + +TEST(DiskTest, Write16) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_write16); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::lba_out_of_range)))) + << "WRITE(16) must fail for a medium with 0 blocks"; + + disk->SetBlockCount(1); + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_write16); + EXPECT_EQ(status::good, controller->GetStatus()); + + // Further testing requires filesystem access +} + +TEST(DiskTest, Verify10) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_verify10); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::lba_out_of_range)))) + << "VERIFY(10) must fail for a medium with 0 blocks"; + + disk->SetReady(true); + // Verify 0 sectors + disk->SetBlockCount(1); + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_verify10); + EXPECT_EQ(status::good, controller->GetStatus()); + + // Verify 1 sector with BytChk=0 + controller->SetCmdByte(8, 1); + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_verify10); + EXPECT_EQ(status::good, controller->GetStatus()); + + // Further testing requires filesystem access +} + +TEST(DiskTest, Verify16) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_verify16); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::lba_out_of_range)))) + << "VERIFY(16) must fail for a medium with 0 blocks"; + + disk->SetReady(true); + // Verify 0 sectors + disk->SetBlockCount(1); + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_verify16); + EXPECT_EQ(status::good, controller->GetStatus()); + + // Verify 1 sector with BytChk=0 + controller->SetCmdByte(13, 1); + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_verify16); + EXPECT_EQ(status::good, controller->GetStatus()); + + // Further testing requires filesystem access +} + +TEST(DiskTest, ReadLong10) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_read_long10); + EXPECT_EQ(status::good, controller->GetStatus()); + + controller->SetCmdByte(2, 1); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_read_long10); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::lba_out_of_range)))) + << "READ LONG(10) must fail because the capacity is exceeded"; + controller->SetCmdByte(2, 0); + + controller->SetCmdByte(7, 1); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_read_long10); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "READ LONG(10) must fail because it currently only supports 0 bytes transfer length"; +} + +TEST(DiskTest, ReadLong16) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + // READ LONG(16), not READ CAPACITY(16) + controller->SetCmdByte(1, 0x11); + controller->SetCmdByte(2, 1); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_read_capacity16_read_long16); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::lba_out_of_range)))) + << "READ LONG(16) must fail because the capacity is exceeded"; + controller->SetCmdByte(2, 0); + + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_read_capacity16_read_long16); + EXPECT_EQ(status::good, controller->GetStatus()); + + controller->SetCmdByte(13, 1); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_read_capacity16_read_long16); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "READ LONG(16) must fail because it currently only supports 0 bytes transfer length"; +} + +TEST(DiskTest, WriteLong10) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_write_long10); + EXPECT_EQ(status::good, controller->GetStatus()); + + controller->SetCmdByte(2, 1); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_write_long10); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::lba_out_of_range)))) + << "WRITE LONG(10) must fail because the capacity is exceeded"; + controller->SetCmdByte(2, 0); + + controller->SetCmdByte(7, 1); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_write_long10); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "WRITE LONG(10) must fail because it currently only supports 0 bytes transfer length"; +} + +TEST(DiskTest, WriteLong16) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + controller->SetCmdByte(2, 1); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_write_long16); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::lba_out_of_range)))) + << "WRITE LONG(16) must fail because the capacity is exceeded"; + controller->SetCmdByte(2, 0); + + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_write_long16); + EXPECT_EQ(status::good, controller->GetStatus()); + + controller->SetCmdByte(13, 1); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_write_long16); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "WRITE LONG(16) must fail because it currently only supports 0 bytes transfer length"; +} + +TEST(DiskTest, StartStopUnit) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + disk->SetRemovable(true); + + // Stop/Unload + disk->SetReady(true); + EXPECT_CALL(*controller, Status); + EXPECT_CALL(*disk, FlushCache); + disk->Dispatch(scsi_command::cmd_start_stop); + EXPECT_EQ(status::good, controller->GetStatus()); + EXPECT_TRUE(disk->IsStopped()); + + // Stop/Load + controller->SetCmdByte(4, 0x02); + disk->SetReady(true); + disk->SetLocked(false); + EXPECT_CALL(*controller, Status); + EXPECT_CALL(*disk, FlushCache); + disk->Dispatch(scsi_command::cmd_start_stop); + EXPECT_EQ(status::good, controller->GetStatus()); + + disk->SetReady(false); + EXPECT_CALL(*disk, FlushCache).Times(0); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_start_stop); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::load_or_eject_failed)))); + + disk->SetReady(true); + disk->SetLocked(true); + EXPECT_CALL(*disk, FlushCache).Times(0); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_start_stop); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::load_or_eject_failed)))); + + // Start/Unload + controller->SetCmdByte(4, 0x01); + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_start_stop); + EXPECT_EQ(status::good, controller->GetStatus()); + EXPECT_FALSE(disk->IsStopped()); + + // Start/Load + controller->SetCmdByte(4, 0x03); + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_start_stop); + EXPECT_EQ(status::good, controller->GetStatus()); +} + +TEST(DiskTest, PreventAllowMediumRemoval) +{ + auto [controller, disk] = CreateDisk(); + // Required by the bullseye clang++ compiler + auto d = disk; + + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_prevent_allow_medium_removal); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::not_ready), + Property(&scsi_exception::get_asc, asc::medium_not_present)))) + << "REMOVAL must fail because drive is not ready"; + + disk->SetReady(true); + + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_prevent_allow_medium_removal); + EXPECT_EQ(status::good, controller->GetStatus()); + EXPECT_FALSE(disk->IsLocked()); + + controller->SetCmdByte(4, 1); + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_prevent_allow_medium_removal); + EXPECT_EQ(status::good, controller->GetStatus()); + EXPECT_TRUE(disk->IsLocked()); +} + +TEST(DiskTest, Eject) +{ + MockDisk disk; + + disk.SetReady(false); + disk.SetRemovable(false); + disk.SetLocked(false); + EXPECT_CALL(disk, FlushCache).Times(0); + EXPECT_FALSE(disk.Eject(false)); + + disk.SetRemovable(true); + EXPECT_CALL(disk, FlushCache).Times(0); + EXPECT_FALSE(disk.Eject(false)); + + disk.SetReady(true); + disk.SetLocked(true); + EXPECT_CALL(disk, FlushCache).Times(0); + EXPECT_FALSE(disk.Eject(false)); + + disk.SetReady(true); + disk.SetLocked(false); + EXPECT_CALL(disk, FlushCache); + EXPECT_TRUE(disk.Eject(false)); + + disk.SetReady(true); + EXPECT_CALL(disk, FlushCache); + EXPECT_TRUE(disk.Eject(true)); +} + +void DiskTest_ValidateFormatPage(AbstractController& controller, int offset) +{ + const auto& buf = controller.GetBuffer(); + EXPECT_EQ(0x08, buf[offset + 3]) << "Wrong number of trackes in one zone"; + EXPECT_EQ(25, GetInt16(buf, offset + 10)) << "Wrong number of sectors per track"; + EXPECT_EQ(1024, GetInt16(buf, offset + 12)) << "Wrong number of bytes per sector"; + EXPECT_EQ(1, GetInt16(buf, offset + 14)) << "Wrong interleave"; + EXPECT_EQ(11, GetInt16(buf, offset + 16)) << "Wrong track skew factor"; + EXPECT_EQ(20, GetInt16(buf, offset + 18)) << "Wrong cylinder skew factor"; + EXPECT_FALSE(buf[offset + 20] & 0x20) << "Wrong removable flag"; + EXPECT_TRUE(buf[offset + 20] & 0x40) << "Wrong hard-sectored flag"; +} + +void DiskTest_ValidateDrivePage(AbstractController& controller, int offset) +{ + const auto& buf = controller.GetBuffer(); + EXPECT_EQ(0x17, buf[offset + 2]); + EXPECT_EQ(0x4d3b, GetInt16(buf, offset + 3)); + EXPECT_EQ(8, buf[offset + 5]) << "Wrong number of heads"; + EXPECT_EQ(7200, GetInt16(buf, offset + 20)) << "Wrong medium rotation rate"; +} + +void DiskTest_ValidateCachePage(AbstractController& controller, int offset) +{ + const auto& buf = controller.GetBuffer(); + EXPECT_EQ(0xffff, GetInt16(buf, offset + 4)) << "Wrong pre-fetch transfer length"; + EXPECT_EQ(0xffff, GetInt16(buf, offset + 8)) << "Wrong maximum pre-fetch"; + EXPECT_EQ(0xffff, GetInt16(buf, offset + 10)) << "Wrong maximum pre-fetch ceiling"; +} + +TEST(DiskTest, ModeSense6) +{ + auto [controller, disk] = CreateDisk(); + + // Drive must be ready on order to return all data + disk->SetReady(true); + + controller->SetCmdByte(2, 0x3f); + // ALLOCATION LENGTH + controller->SetCmdByte(4, 255); + disk->Dispatch(scsi_command::cmd_mode_sense6); + EXPECT_EQ(0x08, controller->GetBuffer()[3]) << "Wrong block descriptor length"; + + // No block descriptor + controller->SetCmdByte(1, 0x08); + disk->Dispatch(scsi_command::cmd_mode_sense6); + EXPECT_EQ(0x00, controller->GetBuffer()[2]) << "Wrong device-specific parameter"; + + disk->SetReadOnly(false); + disk->SetProtectable(true); + disk->SetProtected(true); + disk->Dispatch(scsi_command::cmd_mode_sense6); + const auto& buf = controller->GetBuffer(); + EXPECT_EQ(0x80, buf[2]) << "Wrong device-specific parameter"; + + // Return block descriptor + controller->SetCmdByte(1, 0x00); + + // Format page + controller->SetCmdByte(2, 3); + disk->SetSectorSizeInBytes(1024); + disk->Dispatch(scsi_command::cmd_mode_sense6); + DiskTest_ValidateFormatPage(*controller, 12); + + // Rigid disk drive page + controller->SetCmdByte(2, 4); + disk->SetBlockCount(0x12345678); + disk->Dispatch(scsi_command::cmd_mode_sense6); + DiskTest_ValidateDrivePage(*controller, 12); + + // Cache page + controller->SetCmdByte(2, 8); + disk->Dispatch(scsi_command::cmd_mode_sense6); + DiskTest_ValidateCachePage(*controller, 12); +} + +TEST(DiskTest, ModeSense10) +{ + auto [controller, disk] = CreateDisk(); + + // Drive must be ready on order to return all data + disk->SetReady(true); + + controller->SetCmdByte(2, 0x3f); + // ALLOCATION LENGTH + controller->SetCmdByte(8, 255); + disk->Dispatch(scsi_command::cmd_mode_sense10); + EXPECT_EQ(0x08, controller->GetBuffer()[7]) << "Wrong block descriptor length"; + + // No block descriptor + controller->SetCmdByte(1, 0x08); + disk->Dispatch(scsi_command::cmd_mode_sense10); + auto& buf = controller->GetBuffer(); + EXPECT_EQ(0x00, controller->GetBuffer()[3]) << "Wrong device-specific parameter"; + + disk->SetReadOnly(false); + disk->SetProtectable(true); + disk->SetProtected(true); + disk->Dispatch(scsi_command::cmd_mode_sense10); + buf = controller->GetBuffer(); + EXPECT_EQ(0x80, buf[3]) << "Wrong device-specific parameter"; + + // Return short block descriptor + controller->SetCmdByte(1, 0x00); + disk->SetBlockCount(0x1234); + disk->SetSectorSizeInBytes(1024); + disk->Dispatch(scsi_command::cmd_mode_sense10); + buf = controller->GetBuffer(); + EXPECT_EQ(0x00, buf[4]) << "Wrong LONGLBA field"; + EXPECT_EQ(0x08, buf[7]) << "Wrong block descriptor length"; + EXPECT_EQ(0x00, GetInt16(buf, 8)); + EXPECT_EQ(0x1234, GetInt16(buf, 10)); + EXPECT_EQ(0x00, GetInt16(buf, 12)); + EXPECT_EQ(1024, GetInt16(buf, 14)); + + // Return long block descriptor + controller->SetCmdByte(1, 0x10); + disk->SetBlockCount((uint64_t)0xffffffff + 1); + disk->Dispatch(scsi_command::cmd_mode_sense10); + buf = controller->GetBuffer(); + EXPECT_EQ(0x01, buf[4]) << "Wrong LONGLBA field"; + EXPECT_EQ(0x10, buf[7]) << "Wrong block descriptor length"; + EXPECT_EQ(0x00, GetInt16(buf, 8)); + EXPECT_EQ(0x01, GetInt16(buf, 10)); + EXPECT_EQ(0x00, GetInt16(buf, 12)); + EXPECT_EQ(0x00, GetInt16(buf, 14)); + EXPECT_EQ(0x00, GetInt16(buf, 20)); + EXPECT_EQ(1024, GetInt16(buf, 22)); + + controller->SetCmdByte(1, 0x00); + + // Format page + controller->SetCmdByte(2, 3); + disk->SetSectorSizeInBytes(1024); + disk->Dispatch(scsi_command::cmd_mode_sense10); + DiskTest_ValidateFormatPage(*controller, 16); + + // Rigid disk drive page + controller->SetCmdByte(2, 4); + disk->SetBlockCount(0x12345678); + disk->Dispatch(scsi_command::cmd_mode_sense10); + DiskTest_ValidateDrivePage(*controller, 16); + + // Cache page + controller->SetCmdByte(2, 8); + disk->Dispatch(scsi_command::cmd_mode_sense10); + DiskTest_ValidateCachePage(*controller, 16); +} + +TEST(DiskTest, SynchronizeCache) +{ + auto [controller, disk] = CreateDisk(); + + EXPECT_CALL(*disk, FlushCache); + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_synchronize_cache10); + EXPECT_EQ(status::good, controller->GetStatus()); + + EXPECT_CALL(*disk, FlushCache); + EXPECT_CALL(*controller, Status); + disk->Dispatch(scsi_command::cmd_synchronize_cache16); + EXPECT_EQ(status::good, controller->GetStatus()); +} + +TEST(DiskTest, ReadDefectData) +{ + auto [controller, disk] = CreateDisk(); + + EXPECT_CALL(*controller, DataIn); + disk->Dispatch(scsi_command::cmd_read_defect_data10); + EXPECT_EQ(status::good, controller->GetStatus()); +} + +TEST(DiskTest, SectorSize) +{ + MockDisk disk; + + EXPECT_TRUE(disk.IsSectorSizeConfigurable()); + + disk.SetSectorSizeShiftCount(9); + EXPECT_EQ(9, disk.GetSectorSizeShiftCount()); + EXPECT_EQ(512, disk.GetSectorSizeInBytes()); + disk.SetSectorSizeInBytes(512); + EXPECT_EQ(9, disk.GetSectorSizeShiftCount()); + EXPECT_EQ(512, disk.GetSectorSizeInBytes()); + + disk.SetSectorSizeShiftCount(10); + EXPECT_EQ(10, disk.GetSectorSizeShiftCount()); + EXPECT_EQ(1024, disk.GetSectorSizeInBytes()); + disk.SetSectorSizeInBytes(1024); + EXPECT_EQ(10, disk.GetSectorSizeShiftCount()); + EXPECT_EQ(1024, disk.GetSectorSizeInBytes()); + + disk.SetSectorSizeShiftCount(11); + EXPECT_EQ(11, disk.GetSectorSizeShiftCount()); + EXPECT_EQ(2048, disk.GetSectorSizeInBytes()); + disk.SetSectorSizeInBytes(2048); + EXPECT_EQ(11, disk.GetSectorSizeShiftCount()); + EXPECT_EQ(2048, disk.GetSectorSizeInBytes()); + + disk.SetSectorSizeShiftCount(12); + EXPECT_EQ(12, disk.GetSectorSizeShiftCount()); + EXPECT_EQ(4096, disk.GetSectorSizeInBytes()); + disk.SetSectorSizeInBytes(4096); + EXPECT_EQ(12, disk.GetSectorSizeShiftCount()); + EXPECT_EQ(4096, disk.GetSectorSizeInBytes()); + + EXPECT_THROW(disk.SetSectorSizeInBytes(1234), io_exception); +} + +TEST(DiskTest, ConfiguredSectorSize) +{ + MockScsiHd disk(0, false); + + EXPECT_TRUE(disk.SetConfiguredSectorSize(512)); + EXPECT_EQ(512, disk.GetConfiguredSectorSize()); + + EXPECT_FALSE(disk.SetConfiguredSectorSize(1234)); + EXPECT_EQ(512, disk.GetConfiguredSectorSize()); +} + +TEST(DiskTest, BlockCount) +{ + MockDisk disk; + + disk.SetBlockCount(0x1234567887654321); + EXPECT_EQ(0x1234567887654321, disk.GetBlockCount()); +} diff --git a/cpp/test/host_services_test.cpp b/cpp/test/host_services_test.cpp new file mode 100644 index 00000000..af0b7a6d --- /dev/null +++ b/cpp/test/host_services_test.cpp @@ -0,0 +1,206 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" +#include "shared/shared_exceptions.h" +#include "devices/host_services.h" + +using namespace std; + +void HostServices_SetUpModePages(map>& pages) +{ + EXPECT_EQ(1, pages.size()) << "Unexpected number of mode pages"; + EXPECT_EQ(10, pages[32].size()); +} + +TEST(HostServicesTest, TestUnitReady) +{ + auto [controller, services] = CreateDevice(SCHS); + + EXPECT_CALL(*controller, Status()); + services->Dispatch(scsi_command::cmd_test_unit_ready); + EXPECT_EQ(status::good, controller->GetStatus()); +} + +TEST(HostServicesTest, Inquiry) +{ + TestInquiry::Inquiry(SCHS, device_type::processor, scsi_level::spc_3, "SCSI2Pi Host Services ", 0x1f, false); +} + +TEST(HostServicesTest, StartStopUnit) +{ + auto [controller, services] = CreateDevice(SCHS); + // Required by the bullseye clang++ compiler + auto s = services; + + // STOP + EXPECT_CALL(*controller, Status()); + services->Dispatch(scsi_command::cmd_start_stop); + EXPECT_EQ(status::good, controller->GetStatus()); + + // LOAD + controller->SetCmdByte(4, 0x02); + EXPECT_CALL(*controller, Status()); + services->Dispatch(scsi_command::cmd_start_stop); + EXPECT_EQ(status::good, controller->GetStatus()); + + // UNLOAD + controller->SetCmdByte(4, 0x03); + EXPECT_CALL(*controller, Status()); + services->Dispatch(scsi_command::cmd_start_stop); + EXPECT_EQ(status::good, controller->GetStatus()); + + // START + controller->SetCmdByte(4, 0x01); + EXPECT_THAT([&] { s->Dispatch(scsi_command::cmd_start_stop); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))); +} + +TEST(HostServicesTest, ExecuteOperation) +{ + auto [controller, services] = CreateDevice(SCHS); + // Required by the bullseye clang++ compiler + auto s = services; + + // Illegal format + controller->SetCmdByte(1, 0b000); + EXPECT_THAT([&] { s->Dispatch(scsi_command::cmd_execute_operation); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))); + + // Illegal format + controller->SetCmdByte(1, 0b111); + EXPECT_THAT([&] { s->Dispatch(scsi_command::cmd_execute_operation); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))); + + controller->SetCmdByte(1, 0b001); + // Illegal length + EXPECT_THAT([&] { s->Dispatch(scsi_command::cmd_execute_operation); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))); +} + +TEST(HostServicesTest, ReceiveOperationResults) +{ + auto [controller, services] = CreateDevice(SCHS); + // Required by the bullseye clang++ compiler + auto s = services; + + // Illegal format + controller->SetCmdByte(1, 0b000); + EXPECT_THAT([&] { s->Dispatch(scsi_command::cmd_receive_operation_results); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))); + + // Illegal format + controller->SetCmdByte(1, 0b111); + EXPECT_THAT([&] { s->Dispatch(scsi_command::cmd_receive_operation_results); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))); +} + +TEST(HostServicesTest, ModeSense6) +{ + auto [controller, services] = CreateDevice(SCHS); + // Required by the bullseye clang++ compiler + auto s = services; + + EXPECT_TRUE(services->Init({})); + + EXPECT_THAT([&] { s->Dispatch(scsi_command::cmd_mode_sense6); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "Unsupported mode page was returned"; + + controller->SetCmdByte(2, 0x20); + EXPECT_THAT([&] { s->Dispatch(scsi_command::cmd_mode_sense6); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "Block descriptors are not supported"; + + controller->SetCmdByte(1, 0x08); + // ALLOCATION LENGTH + controller->SetCmdByte(4, 255); + EXPECT_CALL(*controller, DataIn()); + services->Dispatch(scsi_command::cmd_mode_sense6); + vector& buffer = controller->GetBuffer(); + // Major version 1 + EXPECT_EQ(0x01, buffer[6]); + // Minor version 0 + EXPECT_EQ(0x00, buffer[7]); + // Year + EXPECT_NE(0x00, buffer[8]); + // Day + EXPECT_NE(0x00, buffer[10]); + + // ALLOCATION LENGTH + controller->SetCmdByte(4, 2); + EXPECT_CALL(*controller, DataIn()); + services->Dispatch(scsi_command::cmd_mode_sense6); + buffer = controller->GetBuffer(); + EXPECT_EQ(0x02, buffer[0]); +} + +TEST(HostServicesTest, ModeSense10) +{ + auto [controller, services] = CreateDevice(SCHS); + // Required by the bullseye clang++ compiler + auto s = services; + + EXPECT_TRUE(services->Init({})); + + EXPECT_THAT([&] { s->Dispatch(scsi_command::cmd_mode_sense10); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "Unsupported mode page was returned"; + + controller->SetCmdByte(2, 0x20); + EXPECT_THAT([&] { s->Dispatch(scsi_command::cmd_mode_sense10); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "Block descriptors are not supported"; + + controller->SetCmdByte(1, 0x08); + // ALLOCATION LENGTH + controller->SetCmdByte(8, 255); + EXPECT_CALL(*controller, DataIn()); + services->Dispatch(scsi_command::cmd_mode_sense10); + vector& buffer = controller->GetBuffer(); + // Major version 1 + EXPECT_EQ(0x01, buffer[10]); + // Minor version 0 + EXPECT_EQ(0x00, buffer[11]); + // Year + EXPECT_NE(0x00, buffer[12]); + // Day + EXPECT_NE(0x00, buffer[14]); + + // ALLOCATION LENGTH + controller->SetCmdByte(8, 2); + EXPECT_CALL(*controller, DataIn()); + services->Dispatch(scsi_command::cmd_mode_sense10); + buffer = controller->GetBuffer(); + EXPECT_EQ(0x02, buffer[1]); +} + +TEST(HostServicesTest, SetUpModePages) +{ + MockHostServices services(0); + map> pages; + + // Non changeable + services.SetUpModePages(pages, 0x3f, false); + HostServices_SetUpModePages(pages); + + // Changeable + pages.clear(); + services.SetUpModePages(pages, 0x3f, true); + HostServices_SetUpModePages(pages); +} diff --git a/cpp/test/in_process_test.cpp b/cpp/test/in_process_test.cpp new file mode 100644 index 00000000..b05ef44a --- /dev/null +++ b/cpp/test/in_process_test.cpp @@ -0,0 +1,74 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "shared/s2p_util.h" +#include "s2p/s2p_core.h" +#include "s2pdump/s2pdump_core.h" +#include + +using namespace std; +using namespace s2p_util; + +void add_arg(vector &args, const string &arg) +{ + args.push_back(strdup(arg.c_str())); +} + +int main(int argc, char *argv[]) +{ + string t_args; + string i_args; + + int opt; + while ((opt = getopt(argc, argv, "-i:t:")) != -1) { + switch (opt) { + case 'i': + i_args = optarg; + break; + + case 't': + t_args = optarg; + break; + + default: + cerr << "Parser error" << endl; + exit(EXIT_FAILURE); + break; + } + } + + vector initiator_args; + add_arg(initiator_args, "initiator"); + for (const auto &arg : Split(i_args, ' ')) { + add_arg(initiator_args, arg); + } + + vector target_args; + add_arg(target_args, "target"); + for (const auto &arg : Split(t_args, ' ')) { + add_arg(target_args, arg); + } + + +#if !defined __FreeBSD__ && !defined __APPLE__ + auto target_thread = jthread([&target_args]() { +#else + auto target_thread = thread([&target_args]() { +#endif + auto s2p = make_unique(); + s2p->run(target_args, true); + }); + + // TODO Avoid sleeping + sleep(1); + + auto s2pdump = make_unique(); + s2pdump->run(initiator_args, true); + + exit(EXIT_SUCCESS); +} diff --git a/cpp/test/localizer_test.cpp b/cpp/test/localizer_test.cpp new file mode 100644 index 00000000..e12a12eb --- /dev/null +++ b/cpp/test/localizer_test.cpp @@ -0,0 +1,32 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include + +#include "s2p/localizer.h" + +TEST(Localizer, Localize) +{ + Localizer localizer; + + string message = localizer.Localize(LocalizationKey::ERROR_AUTHENTICATION, ""); + EXPECT_FALSE(message.empty()); + EXPECT_EQ(string::npos, message.find("enum value")); + + message = localizer.Localize(LocalizationKey::ERROR_AUTHENTICATION, "de_DE"); + EXPECT_FALSE(message.empty()); + EXPECT_EQ(string::npos, message.find("enum value")); + + message = localizer.Localize(LocalizationKey::ERROR_AUTHENTICATION, "en"); + EXPECT_FALSE(message.empty()); + EXPECT_EQ(string::npos, message.find("enum value")); + + message = localizer.Localize((LocalizationKey)1234, ""); + EXPECT_FALSE(message.empty()); + EXPECT_NE(string::npos, message.find("enum value")); +} diff --git a/cpp/test/mocks.h b/cpp/test/mocks.h new file mode 100644 index 00000000..8c207b58 --- /dev/null +++ b/cpp/test/mocks.h @@ -0,0 +1,408 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include + +#include "test_shared.h" +#include "buses/bus.h" +#include "controllers/scsi_controller.h" +#include "devices/scsi_hd.h" +#include "devices/cd_dvd.h" +#include "devices/optical_memory.h" +#include "devices/host_services.h" +#include "s2p/s2p_executor.h" + +using namespace testing; + +class MockBus : public BUS //NOSONAR Having many fields/methods cannot be avoided +{ +public: + + MOCK_METHOD(bool, Init, (mode_e), (override)); + MOCK_METHOD(void, Reset, (), (override)); + MOCK_METHOD(void, CleanUp, (), (override)); + MOCK_METHOD(bool, GetBSY, (), (const override)); + MOCK_METHOD(void, SetBSY, (bool), (override)); + MOCK_METHOD(bool, GetSEL, (), (const override)); + MOCK_METHOD(void, SetSEL, (bool), (override)); + MOCK_METHOD(bool, GetATN, (), (const override)); + MOCK_METHOD(void, SetATN, (bool), (override)); + MOCK_METHOD(bool, GetACK, (), (const override)); + MOCK_METHOD(void, SetACK, (bool), (override)); + MOCK_METHOD(bool, GetRST, (), (const override)); + MOCK_METHOD(void, SetRST, (bool), (override)); + MOCK_METHOD(bool, GetMSG, (), (const override)); + MOCK_METHOD(void, SetMSG, (bool), (override)); + MOCK_METHOD(bool, GetCD, (), (const override)); + MOCK_METHOD(void, SetCD, (bool), (override)); + MOCK_METHOD(bool, GetIO, (), (override)); + MOCK_METHOD(void, SetIO, (bool), (override)); + MOCK_METHOD(bool, GetREQ, (), (const override)); + MOCK_METHOD(void, SetREQ, (bool), (override)); + MOCK_METHOD(uint8_t, GetDAT, (), (override)); + MOCK_METHOD(void, SetDAT, (uint8_t), (override)); + MOCK_METHOD(uint32_t, Acquire, (), (override)); + MOCK_METHOD(int, CommandHandShake, (vector&), (override)); + MOCK_METHOD(int, ReceiveHandShake, (uint8_t *, int), (override)); + MOCK_METHOD(int, SendHandShake, (uint8_t *, int, int), (override)); + MOCK_METHOD(bool, GetSignal, (int), (const override)); + MOCK_METHOD(void, SetSignal, (int, bool), (override)); + MOCK_METHOD(bool, WaitForSelectEvent, (), (override)); + MOCK_METHOD(void, PinConfig, (int, int), (override)); + MOCK_METHOD(void, PullConfig, (int , int ), (override)); + MOCK_METHOD(void, SetControl, (int , bool ), (override)); + MOCK_METHOD(void, SetMode, (int , int ), (override)); + + MockBus() = default; + ~MockBus() override = default; +}; + +class MockPhaseHandler : public PhaseHandler +{ + FRIEND_TEST(PhaseHandlerTest, Phases); + FRIEND_TEST(PhaseHandlerTest, ProcessPhase); + +public: + + MOCK_METHOD(void, Status, (), (override)); + MOCK_METHOD(void, DataIn, (), (override)); + MOCK_METHOD(void, DataOut, (), (override)); + MOCK_METHOD(void, BusFree, (), (override)); + MOCK_METHOD(void, Selection, (), (override)); + MOCK_METHOD(void, Command, (), (override)); + MOCK_METHOD(void, MsgIn, (), (override)); + MOCK_METHOD(void, MsgOut, (), (override)); + MOCK_METHOD(bool, Process, (int), (override)); + + using PhaseHandler::PhaseHandler; +}; + +inline static const auto mock_bus = make_shared(); + +class MockAbstractController : public AbstractController //NOSONAR Having many fields/methods cannot be avoided +{ + friend class TestInquiry; + + friend shared_ptr CreateDevice(command_interface::PbDeviceType, AbstractController&, int); + + FRIEND_TEST(AbstractControllerTest, AllocateCmd); + FRIEND_TEST(AbstractControllerTest, Reset); + FRIEND_TEST(AbstractControllerTest, DeviceLunLifeCycle); + FRIEND_TEST(AbstractControllerTest, ExtractInitiatorId); + FRIEND_TEST(AbstractControllerTest, GetOpcode); + FRIEND_TEST(AbstractControllerTest, GetLun); + FRIEND_TEST(AbstractControllerTest, Message); + FRIEND_TEST(AbstractControllerTest, Blocks); + FRIEND_TEST(AbstractControllerTest, Length); + FRIEND_TEST(AbstractControllerTest, UpdateOffsetAndLength); + FRIEND_TEST(AbstractControllerTest, Offset); + FRIEND_TEST(AbstractControllerTest, Next); + FRIEND_TEST(AbstractControllerTest, InitBytesToTransfer); + FRIEND_TEST(AbstractControllerTest, ByteTransfer); + FRIEND_TEST(ScsiControllerTest, Selection); + FRIEND_TEST(PrimaryDeviceTest, Inquiry); + FRIEND_TEST(PrimaryDeviceTest, TestUnitReady); + FRIEND_TEST(PrimaryDeviceTest, RequestSense); + FRIEND_TEST(PrimaryDeviceTest, SendDiagnostic); + FRIEND_TEST(PrimaryDeviceTest, ReportLuns); + FRIEND_TEST(PrimaryDeviceTest, UnknownCommand); + FRIEND_TEST(ModePageDeviceTest, ModeSense6); + FRIEND_TEST(ModePageDeviceTest, ModeSense10); + FRIEND_TEST(ModePageDeviceTest, ModeSelect6); + FRIEND_TEST(ModePageDeviceTest, ModeSelect10); + FRIEND_TEST(DiskTest, Dispatch); + FRIEND_TEST(DiskTest, Rezero); + FRIEND_TEST(DiskTest, FormatUnit); + FRIEND_TEST(DiskTest, ReassignBlocks); + FRIEND_TEST(DiskTest, Seek6); + FRIEND_TEST(DiskTest, Seek10); + FRIEND_TEST(DiskTest, Read6); + FRIEND_TEST(DiskTest, Read10); + FRIEND_TEST(DiskTest, Read16); + FRIEND_TEST(DiskTest, Write6); + FRIEND_TEST(DiskTest, Write10); + FRIEND_TEST(DiskTest, Write16); + FRIEND_TEST(DiskTest, Verify10); + FRIEND_TEST(DiskTest, Verify16); + FRIEND_TEST(DiskTest, ReadCapacity10); + FRIEND_TEST(DiskTest, ReadCapacity16); + FRIEND_TEST(DiskTest, ReadLong10); + FRIEND_TEST(DiskTest, ReadLong16); + FRIEND_TEST(DiskTest, WriteLong10); + FRIEND_TEST(DiskTest, WriteLong16); + FRIEND_TEST(DiskTest, PreventAllowMediumRemoval); + FRIEND_TEST(DiskTest, SynchronizeCache); + FRIEND_TEST(DiskTest, ReadDefectData); + FRIEND_TEST(DiskTest, StartStopUnit); + FRIEND_TEST(DiskTest, ModeSense6); + FRIEND_TEST(DiskTest, ModeSense10); + FRIEND_TEST(ScsiDaynaportTest, Read); + FRIEND_TEST(ScsiDaynaportTest, Write); + FRIEND_TEST(ScsiDaynaportTest, Read6); + FRIEND_TEST(ScsiDaynaportTest, Write6); + FRIEND_TEST(ScsiDaynaportTest, TestRetrieveStats); + FRIEND_TEST(ScsiDaynaportTest, SetInterfaceMode); + FRIEND_TEST(ScsiDaynaportTest, SetMcastAddr); + FRIEND_TEST(ScsiDaynaportTest, EnableInterface); + FRIEND_TEST(HostServicesTest, StartStopUnit); + FRIEND_TEST(HostServicesTest, ExecuteOperation); + FRIEND_TEST(HostServicesTest, ReceiveOperationResults); + FRIEND_TEST(HostServicesTest, ModeSense6); + FRIEND_TEST(HostServicesTest, ModeSense10); + FRIEND_TEST(HostServicesTest, SetUpModePages); + FRIEND_TEST(ScsiPrinterTest, Print); + +public: + + MOCK_METHOD(bool, Process, (int), (override)); + MOCK_METHOD(int, GetEffectiveLun, (), (const override)); + MOCK_METHOD(void, Error, (scsi_defs::sense_key, scsi_defs::asc, scsi_defs::status), (override)); + MOCK_METHOD(int, GetInitiatorId, (), (const override)); + MOCK_METHOD(void, Status, (), ()); + MOCK_METHOD(void, DataIn, (), ()); + MOCK_METHOD(void, DataOut, (), ()); + MOCK_METHOD(void, BusFree, (), ()); + MOCK_METHOD(void, Selection, (), ()); + MOCK_METHOD(void, Command, (), ()); + MOCK_METHOD(void, MsgIn, (), ()); + MOCK_METHOD(void, MsgOut, (), ()); + + MockAbstractController() : AbstractController(*mock_bus, 0, 32) { } + explicit MockAbstractController(int target_id) : AbstractController(*mock_bus, target_id, 32) { + SetLength(512); + } + MockAbstractController(shared_ptr bus, int target_id) : AbstractController(*bus, target_id, 32) { + SetLength(512); + } + ~MockAbstractController() override = default; +}; + +class MockScsiController : public ScsiController +{ + FRIEND_TEST(ScsiControllerTest, Process); + FRIEND_TEST(ScsiControllerTest, BusFree); + FRIEND_TEST(ScsiControllerTest, Selection); + FRIEND_TEST(ScsiControllerTest, Command); + FRIEND_TEST(ScsiControllerTest, MsgIn); + FRIEND_TEST(ScsiControllerTest, MsgOut); + FRIEND_TEST(ScsiControllerTest, DataIn); + FRIEND_TEST(ScsiControllerTest, DataOut); + FRIEND_TEST(ScsiControllerTest, Error); + FRIEND_TEST(ScsiControllerTest, RequestSense); + FRIEND_TEST(PrimaryDeviceTest, RequestSense); + +public: + + MOCK_METHOD(void, Reset, (), ()); + MOCK_METHOD(void, Status, (), ()); + MOCK_METHOD(void, Execute, (), ()); + + using ScsiController::ScsiController; + MockScsiController(shared_ptr bus, int target_id) : ScsiController(*bus, target_id, 32) {} + explicit MockScsiController(shared_ptr bus) : ScsiController(*bus, 0, 32) {} + ~MockScsiController() override = default; + +}; + +class MockDevice : public Device +{ + FRIEND_TEST(DeviceTest, Properties); + FRIEND_TEST(DeviceTest, Params); + FRIEND_TEST(DeviceTest, StatusCode); + FRIEND_TEST(DeviceTest, Reset); + FRIEND_TEST(DeviceTest, Start); + FRIEND_TEST(DeviceTest, Stop); + FRIEND_TEST(DeviceTest, Eject); + +public: + + MOCK_METHOD(int, GetId, (), (const)); + + explicit MockDevice(int lun) : Device(UNDEFINED, lun) {} + explicit MockDevice(PbDeviceType type) : Device(type, 0) {} + ~MockDevice() override = default; +}; + +class MockPrimaryDevice : public PrimaryDevice +{ + FRIEND_TEST(PrimaryDeviceTest, PhaseChange); + FRIEND_TEST(PrimaryDeviceTest, TestUnitReady); + FRIEND_TEST(PrimaryDeviceTest, RequestSense); + FRIEND_TEST(PrimaryDeviceTest, Inquiry); + FRIEND_TEST(PrimaryDeviceTest, GetSetSendDelay); + FRIEND_TEST(ScsiControllerTest, RequestSense); + FRIEND_TEST(S2pExecutorTest, ValidateOperationAgainstDevice); + +public: + + MOCK_METHOD(vector, InquiryInternal, (), (const override)); + MOCK_METHOD(void, FlushCache, (), ()); + + explicit MockPrimaryDevice(int lun) : PrimaryDevice(UNDEFINED, lun) {} + ~MockPrimaryDevice() override = default; +}; + +class MockModePageDevice : public ModePageDevice +{ + FRIEND_TEST(ModePageDeviceTest, SupportsSaveParameters); + FRIEND_TEST(ModePageDeviceTest, AddModePages); + FRIEND_TEST(ModePageDeviceTest, AddVendorPage); + +public: + + MOCK_METHOD(vector, InquiryInternal, (), (const override)); + MOCK_METHOD(int, ModeSense6, (span, vector&), (const override)); + MOCK_METHOD(int, ModeSense10, (span, vector&), (const override)); + + MockModePageDevice() : ModePageDevice(UNDEFINED, 0) {} + ~MockModePageDevice() override = default; + + void SetUpModePages(map>& pages, int page, bool) const override { + // Return dummy data for other pages than page 0 + if (page) { + vector buf(32); + pages[page] = buf; + } + } +}; + +class MockPage0ModePageDevice : public MockModePageDevice +{ + FRIEND_TEST(ModePageDeviceTest, Page0); + +public: + + using MockModePageDevice::MockModePageDevice; + + void SetUpModePages(map>& pages, int, bool) const override { + // Return dummy data for pages 0 and 1 + vector buf(32); + pages[0] = buf; + pages[1] = buf; + } +}; + +class MockStorageDevice : public StorageDevice +{ + FRIEND_TEST(StorageDeviceTest, ValidateFile); + FRIEND_TEST(StorageDeviceTest, MediumChanged); + FRIEND_TEST(StorageDeviceTest, GetIdsForReservedFile); + FRIEND_TEST(StorageDeviceTest, FileExists); + FRIEND_TEST(StorageDeviceTest, GetFileSize); + +public: + + MOCK_METHOD(vector, InquiryInternal, (), (const override)); + MOCK_METHOD(void, Open, (), (override)); + MOCK_METHOD(int, ModeSense6, (span, vector&), (const override)); + MOCK_METHOD(int, ModeSense10, (span, vector&), (const override)); + MOCK_METHOD(void, SetUpModePages, ((map>&), int, bool), (const override)); + + MockStorageDevice() : StorageDevice(UNDEFINED, 0) {} + ~MockStorageDevice() override = default; +}; + +class MockDisk : public Disk +{ + FRIEND_TEST(DiskTest, Dispatch); + FRIEND_TEST(DiskTest, Rezero); + FRIEND_TEST(DiskTest, FormatUnit); + FRIEND_TEST(DiskTest, ReassignBlocks); + FRIEND_TEST(DiskTest, Seek6); + FRIEND_TEST(DiskTest, Seek10); + FRIEND_TEST(DiskTest, Read6); + FRIEND_TEST(DiskTest, Read10); + FRIEND_TEST(DiskTest, Read16); + FRIEND_TEST(DiskTest, Write6); + FRIEND_TEST(DiskTest, Write10); + FRIEND_TEST(DiskTest, Write16); + FRIEND_TEST(DiskTest, Verify10); + FRIEND_TEST(DiskTest, Verify16); + FRIEND_TEST(DiskTest, ReadCapacity10); + FRIEND_TEST(DiskTest, ReadCapacity16); + FRIEND_TEST(DiskTest, ReadLong10); + FRIEND_TEST(DiskTest, ReadLong16); + FRIEND_TEST(DiskTest, WriteLong10); + FRIEND_TEST(DiskTest, WriteLong16); + FRIEND_TEST(DiskTest, ReserveRelease); + FRIEND_TEST(DiskTest, SendDiagnostic); + FRIEND_TEST(DiskTest, StartStopUnit); + FRIEND_TEST(DiskTest, PreventAllowMediumRemoval); + FRIEND_TEST(DiskTest, Eject); + FRIEND_TEST(DiskTest, ModeSense6); + FRIEND_TEST(DiskTest, ModeSense10); + FRIEND_TEST(DiskTest, SynchronizeCache); + FRIEND_TEST(DiskTest, ReadDefectData); + FRIEND_TEST(DiskTest, SectorSize); + FRIEND_TEST(DiskTest, BlockCount); + +public: + + MOCK_METHOD(vector, InquiryInternal, (), (const override)); + MOCK_METHOD(void, FlushCache, (), (override)); + MOCK_METHOD(void, Open, (), (override)); + + MockDisk() : Disk(SCHD, 0, { 512, 1024, 2048, 4096 }) {} + ~MockDisk() override = default; +}; + +class MockScsiHd : public ScsiHd //NOSONAR Ignore inheritance hierarchy depth in unit tests +{ + FRIEND_TEST(DiskTest, ConfiguredSectorSize); + FRIEND_TEST(ScsiHdTest, SupportsSaveParameters); + FRIEND_TEST(ScsiHdTest, FinalizeSetup); + FRIEND_TEST(ScsiHdTest, GetProductData); + FRIEND_TEST(ScsiHdTest, SetUpModePages); + FRIEND_TEST(ScsiHdTest, GetSectorSizes); + FRIEND_TEST(ScsiHdTest, ModeSelect); + FRIEND_TEST(S2pExecutorTest, ProcessDeviceCmd); + +public: + + MockScsiHd(int lun, bool removable) : ScsiHd(lun, removable, scsi_level::scsi_2) {} + explicit MockScsiHd(const unordered_set& sector_sizes) : ScsiHd(0, false, scsi_level::scsi_2, sector_sizes) {} + ~MockScsiHd() override = default; +}; + +class MockCdDvd : public CdDvd //NOSONAR Ignore inheritance hierarchy depth in unit tests +{ + FRIEND_TEST(CdDvdTest, GetSectorSizes); + FRIEND_TEST(CdDvdTest, SetUpModePages); + FRIEND_TEST(CdDvdTest, ReadToc); + + using CdDvd::CdDvd; +}; + +class MockOpticalMemory : public OpticalMemory //NOSONAR Ignore inheritance hierarchy depth in unit tests +{ + FRIEND_TEST(OpticalMemoryTest, SupportsSaveParameters); + FRIEND_TEST(OpticalMemoryTest, SetUpModePages); + FRIEND_TEST(OpticalMemoryTest, TestAddVendorPage); + FRIEND_TEST(OpticalMemoryTest, ModeSelect); + + using OpticalMemory::OpticalMemory; +}; + +class MockHostServices : public HostServices +{ + FRIEND_TEST(HostServicesTest, SetUpModePages); + + using HostServices::HostServices; +}; + +class MockS2pExecutor : public S2pExecutor +{ +public: + + MOCK_METHOD(bool, Start, (shared_ptr, bool), (const)); + MOCK_METHOD(bool, Stop, (shared_ptr, bool), (const)); + + using S2pExecutor::S2pExecutor; +}; diff --git a/cpp/test/mode_page_device_test.cpp b/cpp/test/mode_page_device_test.cpp new file mode 100644 index 00000000..562653d5 --- /dev/null +++ b/cpp/test/mode_page_device_test.cpp @@ -0,0 +1,154 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" +#include "shared/shared_exceptions.h" +#include "devices/mode_page_device.h" + +using namespace std; + +TEST(ModePageDeviceTest, SupportsSaveParameters) +{ + MockModePageDevice device; + + EXPECT_FALSE(device.SupportsSaveParameters()) << "Wrong default value"; + device.SupportsSaveParameters(true); + EXPECT_TRUE(device.SupportsSaveParameters()); + device.SupportsSaveParameters(false); + EXPECT_FALSE(device.SupportsSaveParameters()); +} + +TEST(ModePageDeviceTest, AddModePages) +{ + vector cdb(6); + vector buf(512); + MockModePageDevice device; + + // Page 0 + cdb[2] = 0x00; + EXPECT_THAT([&] { device.AddModePages(cdb, buf, 0, 12, 255); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "Data were returned for non-existing mode page 0"; + + // All pages, non changeable + cdb[2] = 0x3f; + EXPECT_EQ(0, device.AddModePages(cdb, buf, 0, 0, 255)); + EXPECT_EQ(3, device.AddModePages(cdb, buf, 0, 3, 255)); + EXPECT_THAT([&] { device.AddModePages(cdb, buf, 0, 12, -1); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "Maximum size was ignored"; + + // All pages, changeable + cdb[2]= 0x7f; + EXPECT_EQ(0, device.AddModePages(cdb, buf, 0, 0, 255)); + EXPECT_EQ(3, device.AddModePages(cdb, buf, 0, 3, 255)); + EXPECT_THAT([&] { device.AddModePages(cdb, buf, 0, 12, -1); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "Maximum size was ignored"; +} + +TEST(ModePageDeviceTest, Page0) +{ + vector cdb(6); + vector buf(512); + MockPage0ModePageDevice device; + + cdb[2] = 0x3f; + EXPECT_EQ(0, device.AddModePages(cdb, buf, 0, 0, 255)); + EXPECT_EQ(1, device.AddModePages(cdb, buf, 0, 1, 255)); +} + +TEST(ModePageDeviceTest, AddVendorPage) +{ + map> pages; + MockModePageDevice device; + + device.AddVendorPage(pages, 0x3f, false); + EXPECT_TRUE(pages.empty()) << "There must not be any default vendor page"; + device.AddVendorPage(pages, 0x3f, true); + EXPECT_TRUE(pages.empty()) << "There must not be any default vendor page"; +} + +TEST(ModePageDeviceTest, ModeSense6) +{ + auto controller = make_shared(0); + auto device = make_shared>(); + EXPECT_TRUE(device->Init({})); + + controller->AddDevice(device); + + EXPECT_CALL(*controller, DataIn()); + device->Dispatch(scsi_command::cmd_mode_sense6); +} + +TEST(ModePageDeviceTest, ModeSense10) +{ + auto controller = make_shared(0); + auto device = make_shared>(); + EXPECT_TRUE(device->Init({})); + + controller->AddDevice(device); + + EXPECT_CALL(*controller, DataIn()); + device->Dispatch(scsi_command::cmd_mode_sense10); +} + +TEST(ModePageDeviceTest, ModeSelect) +{ + MockModePageDevice device; + vector cmd; + vector buf; + + EXPECT_THAT([&] { device.ModeSelect(scsi_command::cmd_mode_select6, cmd, buf, 0); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_command_operation_code)))) + << "Unexpected MODE SELECT(6) default implementation"; + EXPECT_THAT([&] { device.ModeSelect(scsi_command::cmd_mode_select10, cmd, buf, 0); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_command_operation_code)))) + << "Unexpected MODE SELECT(10) default implementation"; +} + +TEST(ModePageDeviceTest, ModeSelect6) +{ + auto controller = make_shared(0); + auto device = make_shared(); + EXPECT_TRUE(device->Init({})); + + controller->AddDevice(device); + + EXPECT_CALL(*controller, DataOut()); + device->Dispatch(scsi_command::cmd_mode_select6); + + controller->SetCmdByte(1, 0x01); + EXPECT_THAT([&] { device->Dispatch(scsi_command::cmd_mode_select6); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "Saving parameters is not supported by base class"; +} + +TEST(ModePageDeviceTest, ModeSelect10) +{ + auto controller = make_shared(0); + auto device = make_shared(); + EXPECT_TRUE(device->Init({})); + + controller->AddDevice(device); + + EXPECT_CALL(*controller, DataOut()); + device->Dispatch(scsi_command::cmd_mode_select10); + + controller->SetCmdByte(1, 0x01); + EXPECT_THAT([&] { device->Dispatch(scsi_command::cmd_mode_select10); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "Saving parameters is not supported for by base class"; +} diff --git a/cpp/test/network_util_test.cpp b/cpp/test/network_util_test.cpp new file mode 100644 index 00000000..8e17d5d4 --- /dev/null +++ b/cpp/test/network_util_test.cpp @@ -0,0 +1,34 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include + +#include +#include +#include "shared/network_util.h" + +using namespace network_util; + +TEST(NetworkUtilTest, IsInterfaceUp) +{ + EXPECT_FALSE(IsInterfaceUp("foo_bar")); +} + +#ifdef __linux__ +TEST(NetworkUtilTest, GetNetworkInterfaces) +{ + EXPECT_FALSE(GetNetworkInterfaces().empty()); +} +#endif + +TEST(NetworkUtilTest, ResolveHostName) +{ + sockaddr_in server_addr = {}; + EXPECT_FALSE(ResolveHostName("foo.foobar", &server_addr)); + EXPECT_TRUE(ResolveHostName("127.0.0.1", &server_addr)); +} diff --git a/cpp/test/optical_memory_test.cpp b/cpp/test/optical_memory_test.cpp new file mode 100644 index 00000000..244b52ff --- /dev/null +++ b/cpp/test/optical_memory_test.cpp @@ -0,0 +1,150 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" + +void ScsiMo_SetUpModePages(map>& pages) +{ + EXPECT_EQ(6, pages.size()) << "Unexpected number of mode pages"; + EXPECT_EQ(12, pages[1].size()); + EXPECT_EQ(24, pages[3].size()); + EXPECT_EQ(24, pages[4].size()); + EXPECT_EQ(4, pages[6].size()); + EXPECT_EQ(12, pages[8].size()); + EXPECT_EQ(12, pages[32].size()); +} + +TEST(OpticalMemoryTest, Inquiry) +{ + TestInquiry::Inquiry(SCMO, device_type::optical_memory, scsi_level::scsi_2, "SCSI2Pi SCSI MO ", 0x1f, true); +} + +TEST(OpticalMemoryTest, SupportsSaveParameters) +{ + MockOpticalMemory mo(0); + + EXPECT_TRUE(mo.SupportsSaveParameters()); +} + +TEST(OpticalMemoryTest, GetSectorSizes) +{ + MockOpticalMemory mo(0); + + const auto& sector_sizes = mo.GetSupportedSectorSizes(); + EXPECT_EQ(4, sector_sizes.size()); + + EXPECT_TRUE(sector_sizes.contains(512)); + EXPECT_TRUE(sector_sizes.contains(1024)); + EXPECT_TRUE(sector_sizes.contains(2048)); + EXPECT_TRUE(sector_sizes.contains(4096)); +} + +TEST(OpticalMemoryTest, SetUpModePages) +{ + map> pages; + MockOpticalMemory mo(0); + + // Non changeable + mo.SetUpModePages(pages, 0x3f, false); + ScsiMo_SetUpModePages(pages); + + // Changeable + pages.clear(); + mo.SetUpModePages(pages, 0x3f, true); + ScsiMo_SetUpModePages(pages); +} + +TEST(OpticalMemoryTest, TestAddVendorPage) +{ + map> pages; + MockOpticalMemory mo(0); + + mo.SetReady(true); + mo.SetUpModePages(pages, 0x21, false); + EXPECT_TRUE(pages.empty()) << "Unsupported vendor-specific page was returned"; + + mo.SetBlockCount(0x12345678); + mo.SetUpModePages(pages, 0x20, false); + EXPECT_EQ(1, pages.size()) << "Unexpected number of mode pages"; + vector& page_32 = pages[32]; + EXPECT_EQ(12, page_32.size()); + EXPECT_EQ(0, to_integer(page_32[2])) << "Wrong format mode"; + EXPECT_EQ(0, to_integer(page_32[3])) << "Wrong format type"; + EXPECT_EQ(0x12345678, GetInt32(page_32, 4)) << "Wrong number of blocks"; + EXPECT_EQ(0, GetInt16(page_32, 8)) << "Wrong number of spare blocks"; + EXPECT_EQ(0, GetInt16(page_32, 10)); + + mo.SetSectorSizeInBytes(512); + mo.SetUpModePages(pages, 0x20, false); + EXPECT_EQ(0, GetInt16(page_32, 8)) << "Wrong number of spare blocks"; + EXPECT_EQ(0, GetInt16(page_32, 10)); + + mo.SetBlockCount(248826); + mo.SetUpModePages(pages, 0x20, false); + EXPECT_EQ(1024, GetInt16(page_32, 8)) << "Wrong number of spare blocks"; + EXPECT_EQ(1, GetInt16(page_32, 10)); + + mo.SetBlockCount(446325); + mo.SetUpModePages(pages, 0x20, false); + EXPECT_EQ(1025, GetInt16(page_32, 8)) << "Wrong number of spare blocks"; + EXPECT_EQ(10, GetInt16(page_32, 10)); + + mo.SetBlockCount(1041500); + mo.SetUpModePages(pages, 0x20, false); + EXPECT_EQ(2250, GetInt16(page_32, 8)) << "Wrong number of spare blocks"; + EXPECT_EQ(18, GetInt16(page_32, 10)); + + mo.SetSectorSizeInBytes(2048); + mo.SetBlockCount(0x12345678); + mo.SetUpModePages(pages, 0x20, false); + EXPECT_EQ(0, GetInt16(page_32, 8)) << "Wrong number of spare blocks"; + EXPECT_EQ(0, GetInt16(page_32, 10)); + + mo.SetBlockCount(310352); + mo.SetUpModePages(pages, 0x20, false); + EXPECT_EQ(2244, GetInt16(page_32, 8)) << "Wrong number of spare blocks"; + EXPECT_EQ(11, GetInt16(page_32, 10)); + + mo.SetBlockCount(605846); + mo.SetUpModePages(pages, 0x20, false); + EXPECT_EQ(4437, GetInt16(page_32, 8)) << "Wrong number of spare blocks"; + EXPECT_EQ(18, GetInt16(page_32, 10)); + + // Changeable page + mo.SetUpModePages(pages, 0x20, true); + EXPECT_EQ(0, to_integer(page_32[2])); + EXPECT_EQ(0, to_integer(page_32[3])); + EXPECT_EQ(0, GetInt32(page_32, 4)); + EXPECT_EQ(0, GetInt16(page_32, 8)); + EXPECT_EQ(0, GetInt16(page_32, 10)); +} + +TEST(OpticalMemoryTest, ModeSelect) +{ + MockOpticalMemory mo(0); + vector cmd(10); + vector buf(255); + + mo.SetSectorSizeInBytes(2048); + + // PF + cmd[1] = 0x10; + // Page 3 (Device Format Page) + buf[4] = 0x03; + // 2048 bytes per sector + buf[16] = 0x08; + EXPECT_NO_THROW(mo.ModeSelect(scsi_command::cmd_mode_select6, cmd, buf, 255)) << "MODE SELECT(6) is supported"; + buf[4] = 0; + buf[16] = 0; + + // Page 3 (Device Format Page) + buf[8] = 0x03; + // 2048 bytes per sector + buf[20] = 0x08; + EXPECT_NO_THROW(mo.ModeSelect(scsi_command::cmd_mode_select10, cmd, buf, 255)) << "MODE SELECT(10) is supported"; +} diff --git a/cpp/test/phase_handler_test.cpp b/cpp/test/phase_handler_test.cpp new file mode 100644 index 00000000..7032718d --- /dev/null +++ b/cpp/test/phase_handler_test.cpp @@ -0,0 +1,139 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" +#include "controllers/phase_handler.h" + +TEST(PhaseHandlerTest, Phases) +{ + MockPhaseHandler handler; + + handler.SetPhase(phase_t::selection); + EXPECT_TRUE(handler.IsSelection()); + EXPECT_FALSE(handler.IsBusFree()); + EXPECT_FALSE(handler.IsCommand()); + EXPECT_FALSE(handler.IsStatus()); + EXPECT_FALSE(handler.IsDataIn()); + EXPECT_FALSE(handler.IsDataOut()); + EXPECT_FALSE(handler.IsMsgIn()); + EXPECT_FALSE(handler.IsMsgOut()); + + handler.SetPhase(phase_t::busfree); + EXPECT_TRUE(handler.IsBusFree()); + EXPECT_FALSE(handler.IsSelection()); + EXPECT_FALSE(handler.IsCommand()); + EXPECT_FALSE(handler.IsStatus()); + EXPECT_FALSE(handler.IsDataIn()); + EXPECT_FALSE(handler.IsDataOut()); + EXPECT_FALSE(handler.IsMsgIn()); + EXPECT_FALSE(handler.IsMsgOut()); + + handler.SetPhase(phase_t::command); + EXPECT_TRUE(handler.IsCommand()); + EXPECT_FALSE(handler.IsBusFree()); + EXPECT_FALSE(handler.IsSelection()); + EXPECT_FALSE(handler.IsStatus()); + EXPECT_FALSE(handler.IsDataIn()); + EXPECT_FALSE(handler.IsDataOut()); + EXPECT_FALSE(handler.IsMsgIn()); + EXPECT_FALSE(handler.IsMsgOut()); + + handler.SetPhase(phase_t::status); + EXPECT_TRUE(handler.IsStatus()); + EXPECT_FALSE(handler.IsBusFree()); + EXPECT_FALSE(handler.IsSelection()); + EXPECT_FALSE(handler.IsCommand()); + EXPECT_FALSE(handler.IsDataIn()); + EXPECT_FALSE(handler.IsDataOut()); + EXPECT_FALSE(handler.IsMsgIn()); + EXPECT_FALSE(handler.IsMsgOut()); + + handler.SetPhase(phase_t::datain); + EXPECT_TRUE(handler.IsDataIn()); + EXPECT_FALSE(handler.IsBusFree()); + EXPECT_FALSE(handler.IsSelection()); + EXPECT_FALSE(handler.IsCommand()); + EXPECT_FALSE(handler.IsStatus()); + EXPECT_FALSE(handler.IsDataOut()); + EXPECT_FALSE(handler.IsMsgIn()); + EXPECT_FALSE(handler.IsMsgOut()); + + handler.SetPhase(phase_t::dataout); + EXPECT_TRUE(handler.IsDataOut()); + EXPECT_FALSE(handler.IsBusFree()); + EXPECT_FALSE(handler.IsSelection()); + EXPECT_FALSE(handler.IsCommand()); + EXPECT_FALSE(handler.IsStatus()); + EXPECT_FALSE(handler.IsDataIn()); + EXPECT_FALSE(handler.IsMsgIn()); + EXPECT_FALSE(handler.IsMsgOut()); + + handler.SetPhase(phase_t::msgin); + EXPECT_TRUE(handler.IsMsgIn()); + EXPECT_FALSE(handler.IsBusFree()); + EXPECT_FALSE(handler.IsSelection()); + EXPECT_FALSE(handler.IsCommand()); + EXPECT_FALSE(handler.IsStatus()); + EXPECT_FALSE(handler.IsDataIn()); + EXPECT_FALSE(handler.IsDataOut()); + EXPECT_FALSE(handler.IsMsgOut()); + + handler.SetPhase(phase_t::msgout); + EXPECT_TRUE(handler.IsMsgOut()); + EXPECT_FALSE(handler.IsBusFree()); + EXPECT_FALSE(handler.IsSelection()); + EXPECT_FALSE(handler.IsCommand()); + EXPECT_FALSE(handler.IsStatus()); + EXPECT_FALSE(handler.IsDataIn()); + EXPECT_FALSE(handler.IsDataOut()); + EXPECT_FALSE(handler.IsMsgIn()); +} + +TEST(PhaseHandlerTest, ProcessPhase) +{ + MockPhaseHandler handler; + handler.Init(); + + handler.SetPhase(phase_t::selection); + EXPECT_CALL(handler, Selection); + EXPECT_TRUE(handler.ProcessPhase()); + + handler.SetPhase(phase_t::busfree); + EXPECT_CALL(handler, BusFree); + EXPECT_TRUE(handler.ProcessPhase()); + + handler.SetPhase(phase_t::datain); + EXPECT_CALL(handler, DataIn); + EXPECT_TRUE(handler.ProcessPhase()); + + handler.SetPhase(phase_t::dataout); + EXPECT_CALL(handler, DataOut); + EXPECT_TRUE(handler.ProcessPhase()); + + handler.SetPhase(phase_t::command); + EXPECT_CALL(handler, Command); + EXPECT_TRUE(handler.ProcessPhase()); + + handler.SetPhase(phase_t::status); + EXPECT_CALL(handler, Status); + EXPECT_TRUE(handler.ProcessPhase()); + + handler.SetPhase(phase_t::msgin); + EXPECT_CALL(handler, MsgIn); + EXPECT_TRUE(handler.ProcessPhase()); + + handler.SetPhase(phase_t::msgout); + EXPECT_CALL(handler, MsgOut); + EXPECT_TRUE(handler.ProcessPhase()); + + handler.SetPhase(phase_t::reselection); + EXPECT_FALSE(handler.ProcessPhase()); + + handler.SetPhase(phase_t::reserved); + EXPECT_FALSE(handler.ProcessPhase()); +} diff --git a/cpp/test/primary_device_test.cpp b/cpp/test/primary_device_test.cpp new file mode 100644 index 00000000..55e15983 --- /dev/null +++ b/cpp/test/primary_device_test.cpp @@ -0,0 +1,356 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" +#include "shared/scsi.h" +#include "shared/shared_exceptions.h" +#include "base/primary_device.h" +#include "base/device_factory.h" +#include "base/scsi_command_util.h" + +using namespace scsi_defs; +using namespace scsi_command_util; + +pair, shared_ptr> CreatePrimaryDevice(int id = 0) +{ + auto controller = make_shared>(id); + auto device = make_shared(0); + EXPECT_TRUE(device->Init({})); + EXPECT_TRUE(controller->AddDevice(device)); + + return { controller, device }; +} + +TEST(PrimaryDeviceTest, GetId) +{ + const int ID = 5; + + auto [controller, device] = CreatePrimaryDevice(ID); + + EXPECT_EQ(ID, device->GetId()); +} + +TEST(PrimaryDeviceTest, PhaseChange) +{ + auto [controller, device] = CreatePrimaryDevice(); + + EXPECT_CALL(*controller, Status); + device->EnterStatusPhase(); + + EXPECT_CALL(*controller, DataIn); + device->EnterDataInPhase(); + + EXPECT_CALL(*controller, DataOut); + device->EnterDataOutPhase(); +} + +TEST(PrimaryDeviceTest, Reset) +{ + auto [controller, device] = CreatePrimaryDevice(); + + device->Dispatch(scsi_command::cmd_reserve6); + EXPECT_FALSE(device->CheckReservation(1, scsi_command::cmd_test_unit_ready, false)) + << "Device must be reserved for initiator ID 1"; + device->Reset(); + EXPECT_TRUE(device->CheckReservation(1, scsi_command::cmd_test_unit_ready, false)) + << "Device must not be reserved anymore for initiator ID 1"; +} + +TEST(PrimaryDeviceTest, CheckReservation) +{ + auto [controller, device] = CreatePrimaryDevice(); + + EXPECT_TRUE(device->CheckReservation(0, scsi_command::cmd_test_unit_ready, false)) + << "Device must not be reserved for initiator ID 0"; + + device->Dispatch(scsi_command::cmd_reserve6); + EXPECT_TRUE(device->CheckReservation(0, scsi_command::cmd_test_unit_ready, false)) + << "Device must not be reserved for initiator ID 0"; + EXPECT_FALSE(device->CheckReservation(1, scsi_command::cmd_test_unit_ready, false)) + << "Device must be reserved for initiator ID 1"; + EXPECT_FALSE(device->CheckReservation(-1, scsi_command::cmd_test_unit_ready, false)) + << "Device must be reserved for unknown initiator"; + EXPECT_TRUE(device->CheckReservation(1, scsi_command::cmd_inquiry, false)) + << "Device must not be reserved for INQUIRY"; + EXPECT_TRUE(device->CheckReservation(1, scsi_command::cmd_request_sense, false)) + << "Device must not be reserved for REQUEST SENSE"; + EXPECT_TRUE(device->CheckReservation(1, scsi_command::cmd_release6, false)) + << "Device must not be reserved for RELEASE (6)"; + + EXPECT_TRUE(device->CheckReservation(1, scsi_command::cmd_prevent_allow_medium_removal, false)) + << "Device must not be reserved for PREVENT ALLOW MEDIUM REMOVAL with prevent bit not set"; + EXPECT_FALSE(device->CheckReservation(1, scsi_command::cmd_prevent_allow_medium_removal, true)) + << "Device must be reserved for PREVENT ALLOW MEDIUM REMOVAL with prevent bit set"; +} + +TEST(PrimaryDeviceTest, ReserveReleaseUnit) +{ + auto [controller, device] = CreatePrimaryDevice(); + + device->Dispatch(scsi_command::cmd_reserve6); + EXPECT_FALSE(device->CheckReservation(1, scsi_command::cmd_test_unit_ready, false)) + << "Device must be reserved for initiator ID 1"; + + device->Dispatch(scsi_command::cmd_release6); + EXPECT_TRUE(device->CheckReservation(1, scsi_command::cmd_test_unit_ready, false)) + << "Device must not be reserved anymore for initiator ID 1"; + + ON_CALL(*controller, GetInitiatorId).WillByDefault(Return(-1)); + device->Dispatch(scsi_command::cmd_reserve6); + EXPECT_FALSE(device->CheckReservation(1, scsi_command::cmd_test_unit_ready, false)) + << "Device must be reserved for unknown initiator"; + + device->Dispatch(scsi_command::cmd_release6); + EXPECT_TRUE(device->CheckReservation(1, scsi_command::cmd_test_unit_ready, false)) + << "Device must not be reserved anymore for unknown initiator"; +} + +TEST(PrimaryDeviceTest, DiscardReservation) +{ + auto [controller, device] = CreatePrimaryDevice(); + + device->Dispatch(scsi_command::cmd_reserve6); + EXPECT_FALSE(device->CheckReservation(1, scsi_command::cmd_test_unit_ready, false)) + << "Device must be reserved for initiator ID 1"; + device->DiscardReservation(); + EXPECT_TRUE(device->CheckReservation(1, scsi_command::cmd_test_unit_ready, false)) + << "Device must not be reserved anymore for initiator ID 1"; +} + +TEST(PrimaryDeviceTest, TestUnitReady) +{ + auto [controller, device] = CreatePrimaryDevice(); + // Required by the bullseye clang++ compiler + auto d = device; + + device->SetReset(true); + device->SetAttn(true); + device->SetReady(false); + EXPECT_CALL(*controller, DataIn).Times(0); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_test_unit_ready); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::unit_attention), + Property(&scsi_exception::get_asc, asc::power_on_or_reset)))); + + device->SetReset(false); + EXPECT_CALL(*controller, DataIn).Times(0); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_test_unit_ready); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::unit_attention), + Property(&scsi_exception::get_asc, asc::not_ready_to_ready_change)))); + + device->SetReset(true); + device->SetAttn(false); + EXPECT_CALL(*controller, DataIn).Times(0); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_test_unit_ready); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::unit_attention), + Property(&scsi_exception::get_asc, asc::power_on_or_reset)))); + + device->SetReset(false); + device->SetAttn(true); + EXPECT_CALL(*controller, DataIn).Times(0); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_test_unit_ready); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::unit_attention), + Property(&scsi_exception::get_asc, asc::not_ready_to_ready_change)))); + + device->SetAttn(false); + EXPECT_CALL(*controller, DataIn).Times(0); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_test_unit_ready); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::not_ready), + Property(&scsi_exception::get_asc, asc::medium_not_present)))); + + device->SetReady(true); + EXPECT_CALL(*controller, Status); + device->Dispatch(scsi_command::cmd_test_unit_ready); + EXPECT_EQ(status::good, controller->GetStatus()); +} + +TEST(PrimaryDeviceTest, Inquiry) +{ + auto [controller, device] = CreatePrimaryDevice(); + // Required by the bullseye clang++ compiler + auto d = device; + + // ALLOCATION LENGTH + controller->SetCmdByte(4, 255); + + ON_CALL(*d, InquiryInternal()).WillByDefault([&d]() { + return d->HandleInquiry(device_type::processor, scsi_level::spc_3, false); + }); + EXPECT_CALL(*device, InquiryInternal); + EXPECT_CALL(*controller, DataIn); + ON_CALL(*controller, GetEffectiveLun()).WillByDefault(Return(1)); + device->Dispatch(scsi_command::cmd_inquiry); + EXPECT_EQ(0x7f, controller->GetBuffer()[0]) << "Invalid LUN was not reported"; + ON_CALL(*controller, GetEffectiveLun()).WillByDefault(Return(0)); + + EXPECT_FALSE(controller->AddDevice(make_shared(0))) << "Duplicate LUN was not rejected"; + EXPECT_CALL(*device, InquiryInternal); + EXPECT_CALL(*controller, DataIn); + device->Dispatch(scsi_command::cmd_inquiry); + EXPECT_EQ(device_type::processor, (device_type)controller->GetBuffer()[0]); + EXPECT_EQ(0x00, controller->GetBuffer()[1]) << "Device was not reported as non-removable"; + EXPECT_EQ(scsi_level::spc_3, (scsi_level)controller->GetBuffer()[2]) << "Wrong SCSI level"; + EXPECT_EQ(scsi_level::scsi_2, (scsi_level)controller->GetBuffer()[3]) << "Wrong response level"; + EXPECT_EQ(0x1f, controller->GetBuffer()[4]) << "Wrong additional data size"; + + ON_CALL(*device, InquiryInternal()).WillByDefault([&d]() { + return d->HandleInquiry(device_type::direct_access, scsi_level::scsi_1_ccs, true); + }); + EXPECT_CALL(*device, InquiryInternal); + EXPECT_CALL(*controller, DataIn); + device->Dispatch(scsi_command::cmd_inquiry); + EXPECT_EQ(device_type::direct_access, (device_type)controller->GetBuffer()[0]); + EXPECT_EQ(0x80, controller->GetBuffer()[1]) << "Device was not reported as removable"; + EXPECT_EQ(scsi_level::scsi_1_ccs, (scsi_level)controller->GetBuffer()[2]) << "Wrong SCSI level"; + EXPECT_EQ(scsi_level::scsi_1_ccs, (scsi_level)controller->GetBuffer()[3]) << "Wrong response level"; + EXPECT_EQ(0x1f, controller->GetBuffer()[4]) << "Wrong additional data size"; + + controller->SetCmdByte(1, 0x01); + EXPECT_CALL(*controller, DataIn).Times(0); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_inquiry); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "EVPD bit is not supported"; + + controller->SetCmdByte(2, 0x01); + EXPECT_CALL(*controller, DataIn).Times(0); + EXPECT_THAT([&d] { d->Dispatch(scsi_command::cmd_inquiry); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "PAGE CODE field is not supported"; + + controller->SetCmdByte(1, 0); + controller->SetCmdByte(2, 0); + // ALLOCATION LENGTH + controller->SetCmdByte(4, 1); + EXPECT_CALL(*device, InquiryInternal); + EXPECT_CALL(*controller, DataIn); + device->Dispatch(scsi_command::cmd_inquiry); + EXPECT_EQ(0x1f, controller->GetBuffer()[4]) << "Wrong additional data size"; + EXPECT_EQ(1, controller->GetLength()) << "Wrong ALLOCATION LENGTH handling"; +} + +TEST(PrimaryDeviceTest, RequestSense) +{ + auto [controller, device] = CreatePrimaryDevice(); + // Required by the bullseye clang++ compiler + auto d = device; + + // ALLOCATION LENGTH + controller->SetCmdByte(4, 255); + + device->SetReady(false); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_request_sense); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::not_ready), + Property(&scsi_exception::get_asc, asc::medium_not_present)))); + + device->SetReady(true); + EXPECT_CALL(*controller, DataIn); + device->Dispatch(scsi_command::cmd_request_sense); + EXPECT_EQ(status::good, controller->GetStatus()); +} + +TEST(PrimaryDeviceTest, SendDiagnostic) +{ + auto [controller, device] = CreatePrimaryDevice(); + // Required by the bullseye clang++ compiler + auto d = device; + + EXPECT_CALL(*controller, Status); + device->Dispatch(scsi_command::cmd_send_diagnostic); + EXPECT_EQ(status::good, controller->GetStatus()); + + controller->SetCmdByte(1, 0x10); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_send_diagnostic); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "SEND DIAGNOSTIC must fail because PF bit is not supported"; + controller->SetCmdByte(1, 0); + + controller->SetCmdByte(3, 1); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_send_diagnostic); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "SEND DIAGNOSTIC must fail because parameter list is not supported"; + controller->SetCmdByte(3, 0); + controller->SetCmdByte(4, 1); + EXPECT_THAT([&] { d->Dispatch(scsi_command::cmd_send_diagnostic); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "SEND DIAGNOSTIC must fail because parameter list is not supported"; +} + +TEST(PrimaryDeviceTest, ReportLuns) +{ + const int LUN1 = 1; + const int LUN2 = 4; + + auto controller = make_shared(0); + auto device1 = make_shared(LUN1); + auto device2 = make_shared(LUN2); + EXPECT_TRUE(device1->Init({})); + EXPECT_TRUE(device2->Init({})); + + controller->AddDevice(device1); + EXPECT_TRUE(controller->HasDeviceForLun(LUN1)); + controller->AddDevice(device2); + EXPECT_TRUE(controller->HasDeviceForLun(LUN2)); + + // ALLOCATION LENGTH + controller->SetCmdByte(9, 255); + + EXPECT_CALL(*controller, DataIn); + device1->Dispatch(scsi_command::cmd_reportLuns); + const vector& buffer = controller->GetBuffer(); + EXPECT_EQ(0, GetInt16(buffer, 0)) << "Wrong data length"; + EXPECT_EQ(16, GetInt16(buffer, 2)) << "Wrong data length"; + EXPECT_EQ(0, GetInt16(buffer, 8)) << "Wrong LUN1 number"; + EXPECT_EQ(0, GetInt16(buffer, 10)) << "Wrong LUN1 number"; + EXPECT_EQ(0, GetInt16(buffer, 12)) << "Wrong LUN1 number"; + EXPECT_EQ(LUN1, GetInt16(buffer, 14)) << "Wrong LUN1 number"; + EXPECT_EQ(0, GetInt16(buffer, 16)) << "Wrong LUN2 number"; + EXPECT_EQ(0, GetInt16(buffer, 18)) << "Wrong LUN2 number"; + EXPECT_EQ(0, GetInt16(buffer, 20)) << "Wrong LUN2 number"; + EXPECT_EQ(LUN2, GetInt16(buffer, 22)) << "Wrong LUN2 number"; + + controller->SetCmdByte(2, 0x01); + EXPECT_THAT([&] { device1->Dispatch(scsi_command::cmd_reportLuns); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "Only SELECT REPORT mode 0 is supported"; +} + +TEST(PrimaryDeviceTest, Dispatch) +{ + auto [controller, device] = CreatePrimaryDevice(); + + EXPECT_THROW(device->Dispatch(static_cast(0x1f)), scsi_exception) << "Unknown command"; +} + +TEST(PrimaryDeviceTest, WriteByteSequence) +{ + auto [controller, device] = CreatePrimaryDevice(); + + EXPECT_FALSE(device->WriteByteSequence({})) << "Primary device does not support writing byte sequences"; +} + +TEST(PrimaryDeviceTest, GetSetSendDelay) +{ + MockPrimaryDevice device(0); + + EXPECT_EQ(-1, device.GetSendDelay()) << "Wrong delay default value"; + device.SetSendDelay(1234); + EXPECT_EQ(1234, device.GetSendDelay()); +} + +TEST(PrimaryDeviceTest, Init) +{ + param_map params; + MockPrimaryDevice device(0); + + EXPECT_TRUE(device.Init(params)) << "Initialization of primary device must not fail"; +} diff --git a/cpp/test/printer_test.cpp b/cpp/test/printer_test.cpp new file mode 100644 index 00000000..54f9f53b --- /dev/null +++ b/cpp/test/printer_test.cpp @@ -0,0 +1,122 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" +#include "shared/shared_exceptions.h" +#include "devices/printer.h" + +using namespace std; + +TEST(ScsiPrinterTest, GetDefaultParams) +{ + const auto [controller, printer] = CreateDevice(SCLP); + const auto params = printer->GetDefaultParams(); + EXPECT_EQ(1, params.size()); +} + +TEST(ScsiPrinterTest, Init) +{ + auto [controller, printer] = CreateDevice(SCLP); + EXPECT_TRUE(printer->Init({})); + + param_map params; + params["cmd"] = "missing_filename_specifier"; + EXPECT_FALSE(printer->Init(params)); + + params["cmd"] = "%f"; + EXPECT_TRUE(printer->Init(params)); +} + +TEST(ScsiPrinterTest, TestUnitReady) +{ + auto [controller, printer] = CreateDevice(SCLP); + + EXPECT_CALL(*controller, Status()); + printer->Dispatch(scsi_command::cmd_test_unit_ready); + EXPECT_EQ(status::good, controller->GetStatus()); +} + +TEST(ScsiPrinterTest, Inquiry) +{ + TestInquiry::Inquiry(SCLP, device_type::printer, scsi_level::scsi_2, "SCSI2Pi SCSI PRINTER ", 0x1f, false); +} + +TEST(ScsiPrinterTest, ReserveUnit) +{ + auto [controller, printer] = CreateDevice(SCLP); + + EXPECT_CALL(*controller, Status()).Times(1); + printer->Dispatch(scsi_command::cmd_reserve6); + EXPECT_EQ(status::good, controller->GetStatus()); +} + +TEST(ScsiPrinterTest, ReleaseUnit) +{ + auto [controller, printer] = CreateDevice(SCLP); + + EXPECT_CALL(*controller, Status()).Times(1); + printer->Dispatch(scsi_command::cmd_release6); + EXPECT_EQ(status::good, controller->GetStatus()); +} + +TEST(ScsiPrinterTest, SendDiagnostic) +{ + auto [controller, printer] = CreateDevice(SCLP); + + EXPECT_CALL(*controller, Status()).Times(1); + printer->Dispatch(scsi_command::cmd_send_diagnostic); + EXPECT_EQ(status::good, controller->GetStatus()); +} + +TEST(ScsiPrinterTest, Print) +{ + auto [controller, printer] = CreateDevice(SCLP); + // Required by the bullseye clang++ compiler + auto p = printer; + + EXPECT_CALL(*controller, DataOut()); + printer->Dispatch(scsi_command::cmd_print); + + controller->SetCmdByte(3, 0xff); + controller->SetCmdByte(4, 0xff); + EXPECT_THAT([&] { p->Dispatch(scsi_command::cmd_print); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_cdb)))) + << "Buffer overflow was not reported"; +} + +TEST(ScsiPrinterTest, StopPrint) +{ + auto [controller, printer] = CreateDevice(SCLP); + + EXPECT_CALL(*controller, Status()); + printer->Dispatch(scsi_command::cmd_stop_print); + EXPECT_EQ(status::good, controller->GetStatus()); +} + +TEST(ScsiPrinterTest, Synchronize_buffer) +{ + auto [controller, printer] = CreateDevice(SCLP); + // Required by the bullseye clang++ compiler + auto p = printer; + + EXPECT_THAT([&] { p->Dispatch(scsi_command::cmd_synchronize_buffer); }, Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::aborted_command), + Property(&scsi_exception::get_asc, asc::no_additional_sense_information)))) + << "Nothing to print"; + + // Further testing would use the printing system +} + +TEST(ScsiPrinterTest, WriteByteSequence) +{ + auto [controller, printer] = CreateDevice(SCLP); + + const vector buf(1); + EXPECT_TRUE(printer->WriteByteSequence(buf)); +} diff --git a/cpp/test/protobuf_util_test.cpp b/cpp/test/protobuf_util_test.cpp new file mode 100644 index 00000000..f0a07d07 --- /dev/null +++ b/cpp/test/protobuf_util_test.cpp @@ -0,0 +1,240 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" +#include "shared/protobuf_util.h" +#include "shared/shared_exceptions.h" +#include "generated/command_interface.pb.h" +#include + +using namespace command_interface; +using namespace protobuf_util; + +void TestSpecialDevice(const string& name) +{ + PbDeviceDefinition device; + ParseParameters(device, name); + EXPECT_EQ(name, GetParam(device, "file")); + EXPECT_EQ("", GetParam(device, "interfaces")); +} + +TEST(ProtobufUtil, GetSetParam) +{ + // The implementation is a function template, testing one possible T is sufficient + PbCommand command; + SetParam(command, "key", "value"); + EXPECT_EQ("value", GetParam(command, "key")); + EXPECT_EQ("", GetParam(command, "xyz")); + EXPECT_EQ("", GetParam(command, "")); +} + +TEST(ProtobufUtil, ParseParameters) +{ + PbDeviceDefinition device1; + ParseParameters(device1, "a=b:c=d:e"); + EXPECT_EQ("b", GetParam(device1, "a")); + EXPECT_EQ("d", GetParam(device1, "c")); + EXPECT_EQ("", GetParam(device1, "e")); + + // Old style parameter + PbDeviceDefinition device2; + ParseParameters(device2, "a"); + EXPECT_EQ("a", GetParam(device2, "file")); + + TestSpecialDevice("bridge"); + TestSpecialDevice("daynaport"); + TestSpecialDevice("printer"); + TestSpecialDevice("services"); +} + +TEST(ProtobufUtil, SetCommandParams) +{ + PbCommand command1; + SetCommandParams(command1, "file"); + EXPECT_EQ("", GetParam(command1, "folder_pattern")); + EXPECT_EQ("file", GetParam(command1, "file_pattern")); + + PbCommand command2; + SetCommandParams(command2, ":file"); + EXPECT_EQ("", GetParam(command2, "folder_pattern")); + EXPECT_EQ("file", GetParam(command2, "file_pattern")); + + PbCommand command3; + SetCommandParams(command3, "file:"); + EXPECT_EQ("file", GetParam(command3, "file_pattern")); + EXPECT_EQ("", GetParam(command3, "folder_pattern")); + + PbCommand command4; + SetCommandParams(command4, "folder:file"); + EXPECT_EQ("folder", GetParam(command4, "folder_pattern")); + EXPECT_EQ("file", GetParam(command4, "file_pattern")); + + PbCommand command5; + SetCommandParams(command5, "folder:file:"); + EXPECT_EQ("folder", GetParam(command5, "folder_pattern")); + EXPECT_EQ("file", GetParam(command5, "file_pattern")); + + PbCommand command6; + SetCommandParams(command6, "folder:file:operations"); + EXPECT_EQ("folder", GetParam(command6, "folder_pattern")); + EXPECT_EQ("file", GetParam(command6, "file_pattern")); + EXPECT_EQ("operations", GetParam(command6, "operations")); +} + +TEST(ProtobufUtil, SetFromGenericParams) +{ + PbCommand command1; + EXPECT_TRUE(SetFromGenericParams(command1, "operations=mapping_info:folder_pattern=pattern").empty()); + EXPECT_EQ("mapping_info", GetParam(command1, "operations")); + EXPECT_EQ("pattern", GetParam(command1, "folder_pattern")); + + PbCommand command2; + EXPECT_FALSE(SetFromGenericParams(command2, "=mapping_info").empty()); + + PbCommand command3; + EXPECT_FALSE(SetFromGenericParams(command3, "=").empty()); +} + +TEST(ProtobufUtil, ListDevices) +{ + vector devices; + + EXPECT_FALSE(ListDevices(devices).empty()); + + PbDevice device; + device.set_type(SCHD); + devices.push_back(device); + device.set_type(SCDP); + devices.push_back(device); + device.set_type(SCHS); + devices.push_back(device); + device.set_type(SCLP); + devices.push_back(device); + const string device_list = ListDevices(devices); + EXPECT_FALSE(device_list.empty()); + EXPECT_NE(string::npos, device_list.find("DaynaPort SCSI/Link")); + EXPECT_NE(string::npos, device_list.find("Host Services")); + EXPECT_NE(string::npos, device_list.find("SCSI Printer")); +} + +TEST(ProtobufUtil, SetProductData) +{ + PbDeviceDefinition device; + + SetProductData(device, ""); + EXPECT_EQ("", device.vendor()); + EXPECT_EQ("", device.product()); + EXPECT_EQ("", device.revision()); + + SetProductData(device, "vendor"); + EXPECT_EQ("vendor", device.vendor()); + EXPECT_EQ("", device.product()); + EXPECT_EQ("", device.revision()); + + SetProductData(device, "vendor:product"); + EXPECT_EQ("vendor", device.vendor()); + EXPECT_EQ("product", device.product()); + EXPECT_EQ("", device.revision()); + + SetProductData(device, "vendor:product:revision"); + EXPECT_EQ("vendor", device.vendor()); + EXPECT_EQ("product", device.product()); + EXPECT_EQ("revision", device.revision()); +} + +TEST(ProtobufUtil, SetIdAndLun) +{ + PbDeviceDefinition device; + + EXPECT_NE("", SetIdAndLun(8, 32, device, "")); + EXPECT_EQ("", SetIdAndLun(8, 32, device, "1")); + EXPECT_EQ(1, device.id()); + EXPECT_EQ("", SetIdAndLun(8, 32, device, "2:0")); + EXPECT_EQ(2, device.id()); + EXPECT_EQ(0, device.unit()); +} + +TEST(ProtobufUtil, SerializeMessage) +{ + PbResult result; + + const int fd = open("/dev/null", O_WRONLY); + ASSERT_NE(-1, fd); + SerializeMessage(fd, result); + close(fd); + EXPECT_THROW(SerializeMessage(-1, result), io_exception) << "Writing a message must fail"; +} + +TEST(ProtobufUtil, DeserializeMessage) +{ + PbResult result; + vector buf(1); + + int fd = open("/dev/null", O_RDONLY); + ASSERT_NE(-1, fd); + EXPECT_THROW(DeserializeMessage(fd, result), io_exception) << "Reading the message header must fail"; + close(fd); + + auto [fd1, filename1] = OpenTempFile(); + // Data size -1 + buf = { byte{0xff}, byte{0xff}, byte{0xff}, byte{0xff} }; + EXPECT_EQ(buf.size(), write(fd1, buf.data(), buf.size())); + close(fd1); + fd1 = open(filename1.c_str(), O_RDONLY); + ASSERT_NE(-1, fd1); + EXPECT_THROW(DeserializeMessage(fd1, result), io_exception) << "Invalid header was not rejected"; + remove(filename1); + + auto [fd2, filename2] = OpenTempFile(); + // Data size 2 + buf = { byte{0x02}, byte{0x00}, byte{0x00}, byte{0x00} }; + EXPECT_EQ(buf.size(), write(fd2, buf.data(), buf.size())); + close(fd2); + fd2 = open(filename2.c_str(), O_RDONLY); + EXPECT_NE(-1, fd2); + EXPECT_THROW(DeserializeMessage(fd2, result), io_exception) << "Invalid data were not rejected"; + remove(filename2); +} + +TEST(ProtobufUtil, SerializeDeserializeMessage) +{ + PbResult result; + result.set_status(true); + + auto [fd, filename] = OpenTempFile(); + ASSERT_NE(-1, fd); + SerializeMessage(fd, result); + close(fd); + + result.set_status(false); + fd = open(filename.c_str(), O_RDONLY); + ASSERT_NE(-1, fd); + DeserializeMessage(fd, result); + close(fd); + remove(filename); + + EXPECT_TRUE(result.status()); +} + +TEST(ProtobufUtil, ReadBytes) +{ + vector buf1(1); + vector buf2; + + int fd = open("/dev/null", O_RDONLY); + ASSERT_NE(-1, fd); + EXPECT_EQ(0, ReadBytes(fd, buf1)); + EXPECT_EQ(0, ReadBytes(fd, buf2)); + close(fd); + + fd = open("/dev/zero", O_RDONLY); + ASSERT_NE(-1, fd); + EXPECT_EQ(1, ReadBytes(fd, buf1)); + EXPECT_EQ(0, ReadBytes(fd, buf2)); + close(fd); +} diff --git a/cpp/test/s2p_executor_test.cpp b/cpp/test/s2p_executor_test.cpp new file mode 100644 index 00000000..dcc438bf --- /dev/null +++ b/cpp/test/s2p_executor_test.cpp @@ -0,0 +1,616 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" +#include "shared/protobuf_util.h" +#include "shared/shared_exceptions.h" +#include "controllers/controller_factory.h" +#include "base/device_factory.h" +#include "generated/command_interface.pb.h" +#include "s2p/command_context.h" +#include "s2p/s2p_response.h" +#include "s2p/s2p_executor.h" +#include + +using namespace filesystem; +using namespace command_interface; +using namespace protobuf_util; + +TEST(S2pExecutorTest, ProcessDeviceCmd) +{ + const int ID = 3; + const int LUN = 0; + + auto bus = make_shared(); + MockAbstractController controller(bus, ID); + auto controller_factory = make_shared(); + auto executor = make_shared(*bus, controller_factory); + PbDeviceDefinition definition; + PbCommand command; + CommandContext context(command, "", ""); + + definition.set_id(8); + definition.set_unit(32); + EXPECT_FALSE(executor->ProcessDeviceCmd(context, definition, true)) << "Invalid ID and LUN must fail"; + + definition.set_unit(LUN); + EXPECT_FALSE(executor->ProcessDeviceCmd(context, definition, true)) << "Invalid ID must fail"; + + definition.set_id(ID); + definition.set_unit(32); + EXPECT_FALSE(executor->ProcessDeviceCmd(context, definition, true)) << "Invalid LUN must fail"; + + definition.set_unit(LUN); + EXPECT_FALSE(executor->ProcessDeviceCmd(context, definition, true)) << "Unknown operation must fail"; + + command.set_operation(ATTACH); + CommandContext context_attach(command, "", ""); + EXPECT_FALSE(executor->ProcessDeviceCmd(context_attach, definition, true)) << "Operation for unknown device type must fail"; + + auto device1 = make_shared(LUN); + EXPECT_TRUE(controller_factory->AttachToController(*bus, ID, device1)); + + definition.set_type(SCHS); + command.set_operation(INSERT); + CommandContext context_insert1(command, "", ""); + EXPECT_FALSE(executor->ProcessDeviceCmd(context_insert1, definition, true)) << "Operation unsupported by device must fail"; + controller_factory->DeleteAllControllers(); + definition.set_type(SCRM); + + auto device2 = make_shared(LUN, false); + device2->SetRemovable(true); + device2->SetProtectable(true); + device2->SetReady(true); + EXPECT_TRUE(controller_factory->AttachToController(*bus, ID, device2)); + + EXPECT_FALSE(executor->ProcessDeviceCmd(context_attach, definition, true)) << "ID and LUN already exist"; + + command.set_operation(START); + CommandContext context_start(command, "", ""); + EXPECT_TRUE(executor->ProcessDeviceCmd(context_start, definition, true)); + EXPECT_TRUE(executor->ProcessDeviceCmd(context_start, definition, false)); + + command.set_operation(PROTECT); + CommandContext context_protect(command, "", ""); + EXPECT_TRUE(executor->ProcessDeviceCmd(context_protect, definition, true)); + EXPECT_TRUE(executor->ProcessDeviceCmd(context_protect, definition, false)); + + command.set_operation(UNPROTECT); + CommandContext context_unprotect(command, "", ""); + EXPECT_TRUE(executor->ProcessDeviceCmd(context_unprotect, definition, true)); + EXPECT_TRUE(executor->ProcessDeviceCmd(context_unprotect, definition, false)); + + command.set_operation(STOP); + CommandContext context_stop(command, "", ""); + EXPECT_TRUE(executor->ProcessDeviceCmd(context_stop, definition, true)); + EXPECT_TRUE(executor->ProcessDeviceCmd(context_stop, definition, false)); + + command.set_operation(EJECT); + CommandContext context_eject(command, "", ""); + EXPECT_TRUE(executor->ProcessDeviceCmd(context_eject, definition, true)); + EXPECT_TRUE(executor->ProcessDeviceCmd(context_eject, definition, false)); + + command.set_operation(INSERT); + SetParam(definition, "file", "filename"); + CommandContext context_insert2(command, "", ""); + EXPECT_FALSE(executor->ProcessDeviceCmd(context_insert2, definition, true)) << "Non-existing file"; + EXPECT_FALSE(executor->ProcessDeviceCmd(context_insert2, definition, false)) << "Non-existing file"; + + command.set_operation(DETACH); + CommandContext context_detach(command, "", ""); + EXPECT_TRUE(executor->ProcessDeviceCmd(context_detach, definition, true)); + EXPECT_TRUE(executor->ProcessDeviceCmd(context_detach, definition, false)); +} + +TEST(S2pExecutorTest, ProcessCmd) +{ + auto bus = make_shared(); + MockAbstractController controller(bus, 0); + auto controller_factory = make_shared(); + auto executor = make_shared(*bus, controller_factory); + + PbCommand command_detach_all; + command_detach_all.set_operation(DETACH_ALL); + CommandContext context_detach_all(command_detach_all, "", ""); + EXPECT_TRUE(executor->ProcessCmd(context_detach_all)); + + PbCommand command_reserve_ids1; + command_reserve_ids1.set_operation(RESERVE_IDS); + SetParam(command_reserve_ids1, "ids", "2,3"); + CommandContext context_reserve_ids1(command_reserve_ids1, "", ""); + EXPECT_TRUE(executor->ProcessCmd(context_reserve_ids1)); + const unordered_set ids = executor->GetReservedIds(); + EXPECT_EQ(2, ids.size()); + EXPECT_TRUE(ids.contains(2)); + EXPECT_TRUE(ids.contains(3)); + + PbCommand command_reserve_ids2; + command_reserve_ids2.set_operation(RESERVE_IDS); + CommandContext context_reserve_ids2(command_reserve_ids2, "", ""); + EXPECT_TRUE(executor->ProcessCmd(context_reserve_ids2)); + EXPECT_TRUE(executor->GetReservedIds().empty()); + + PbCommand command_reserve_ids3; + command_reserve_ids3.set_operation(RESERVE_IDS); + SetParam(command_reserve_ids3, "ids", "-1"); + CommandContext context_reserve_ids3(command_reserve_ids3, "", ""); + EXPECT_FALSE(executor->ProcessCmd(context_reserve_ids3)); + EXPECT_TRUE(executor->GetReservedIds().empty()); + + PbCommand command_no_operation; + command_no_operation.set_operation(NO_OPERATION); + CommandContext context_no_operation(command_no_operation, "", ""); + EXPECT_TRUE(executor->ProcessCmd(context_no_operation)); + + PbCommand command_attach1; + command_attach1.set_operation(ATTACH); + auto device1 = command_attach1.add_devices(); + device1->set_type(SCHS); + device1->set_id(-1); + CommandContext context_attach1(command_attach1, "", ""); + EXPECT_FALSE(executor->ProcessCmd(context_attach1)); + + PbCommand command_attach2; + command_attach2.set_operation(ATTACH); + auto device2 = command_attach2.add_devices(); + device2->set_type(SCHS); + device2->set_id(0); + device2->set_unit(1); + CommandContext context_attach2(command_attach2, "", ""); + EXPECT_FALSE(executor->ProcessCmd(context_attach2)) << "LUN 0 is missing"; +} + +TEST(S2pExecutorTest, Attach) +{ + const int ID = 3; + const int LUN = 0; + + DeviceFactory device_factory; + auto bus = make_shared(); + auto controller_factory = make_shared(); + auto executor = make_shared(*bus, controller_factory); + PbDeviceDefinition definition; + PbCommand command; + CommandContext context(command, "", ""); + + definition.set_unit(32); + EXPECT_FALSE(executor->Attach(context, definition, false)); + + auto device = device_factory.CreateDevice(SCHD, LUN, ""); + definition.set_id(ID); + definition.set_unit(LUN); + + executor->SetReservedIds(to_string(ID)); + EXPECT_FALSE(executor->Attach(context, definition, false)) << "Reserved ID not rejected"; + + executor->SetReservedIds(""); + EXPECT_FALSE(executor->Attach(context, definition, false)) << "Unknown device type not rejected"; + + definition.set_type(PbDeviceType::SCHS); + EXPECT_TRUE(executor->Attach(context, definition, false)); + controller_factory->DeleteAllControllers(); + + definition.set_type(PbDeviceType::SCHD); + EXPECT_FALSE(executor->Attach(context, definition, false)) << "Drive without sectors not rejected"; + + definition.set_revision("invalid revision"); + EXPECT_FALSE(executor->Attach(context, definition, false)) << "Drive with invalid revision not rejected"; + definition.set_revision("1234"); + + definition.set_block_size(1); + EXPECT_FALSE(executor->Attach(context, definition, false)) << "Drive with invalid sector size not rejected"; + + definition.set_block_size(512); + EXPECT_FALSE(executor->Attach(context, definition, false)) << "Drive without image file not rejected"; + + SetParam(definition, "file", "/non_existing_file"); + EXPECT_FALSE(executor->Attach(context, definition, false)) << "Drive with non-existing image file not rejected"; + + path filename = CreateTempFile(1); + SetParam(definition, "file", filename.string()); + EXPECT_FALSE(executor->Attach(context, definition, false)) << "Too small image file not rejected"; + remove(filename); + + filename = CreateTempFile(512); + SetParam(definition, "file", filename.string()); + bool result = executor->Attach(context, definition, false); + remove(filename); + EXPECT_TRUE(result); + controller_factory->DeleteAllControllers(); + + filename = CreateTempFile(513); + SetParam(definition, "file", filename.string()); + result = executor->Attach(context, definition, false); + remove(filename); + EXPECT_TRUE(result); + + definition.set_type(PbDeviceType::SCCD); + definition.set_unit(LUN + 1); + filename = CreateTempFile(2048); + SetParam(definition, "file", filename.string()); + result = executor->Attach(context, definition, false); + remove(filename); + EXPECT_TRUE(result); + + definition.set_type(PbDeviceType::SCMO); + definition.set_unit(LUN + 2); + SetParam(definition, "read_only", "true"); + filename = CreateTempFile(4096); + SetParam(definition, "file", filename.string()); + result = executor->Attach(context, definition, false); + remove(filename); + EXPECT_TRUE(result); + + controller_factory->DeleteAllControllers(); +} + +TEST(S2pExecutorTest, Insert) +{ + auto bus = make_shared(); + auto [controller, device] = CreateDevice(SCHD); + auto controller_factory = make_shared(); + auto executor = make_shared(*bus, controller_factory); + PbDeviceDefinition definition; + PbCommand command; + CommandContext context(command, "", ""); + + device->SetRemoved(false); + EXPECT_FALSE(executor->Insert(context, definition, device, false)) << "Medium is not removed"; + + device->SetRemoved(true); + definition.set_vendor("v"); + EXPECT_FALSE(executor->Insert(context, definition, device, false)) << "Product data must not be set"; + definition.set_vendor(""); + + definition.set_product("p"); + EXPECT_FALSE(executor->Insert(context, definition, device, false)) << "Product data must not be set"; + definition.set_product(""); + + definition.set_revision("r"); + EXPECT_FALSE(executor->Insert(context, definition, device, false)) << "Product data must not be set"; + definition.set_revision(""); + + EXPECT_FALSE(executor->Insert(context, definition, device, false)) << "Filename is missing"; + + SetParam(definition, "file", "filename"); + EXPECT_TRUE(executor->Insert(context, definition, device, true)) << "Dry-run must not fail"; + EXPECT_FALSE(executor->Insert(context, definition, device, false)); + + definition.set_block_size(1); + EXPECT_FALSE(executor->Insert(context, definition, device, false)); + + definition.set_block_size(0); + EXPECT_FALSE(executor->Insert(context, definition, device, false)) << "Image file validation must fail"; + + SetParam(definition, "file", "/non_existing_file"); + EXPECT_FALSE(executor->Insert(context, definition, device, false)); + + path filename = CreateTempFile(1); + SetParam(definition, "file", filename.string()); + EXPECT_FALSE(executor->Insert(context, definition, device, false)) << "Too small image file not rejected"; + remove(filename); + + filename = CreateTempFile(512); + SetParam(definition, "file", filename.string()); + const bool result = executor->Insert(context, definition, device, false); + remove(filename); + EXPECT_TRUE(result); +} + +TEST(S2pExecutorTest, Detach) +{ + const int ID = 3; + const int LUN1 = 0; + const int LUN2 = 1; + + DeviceFactory device_factory; + auto bus = make_shared(); + auto controller_factory = make_shared(); + auto executor = make_shared(*bus, controller_factory); + PbCommand command; + CommandContext context(command, "", ""); + + auto device1 = device_factory.CreateDevice(SCHS, LUN1, ""); + EXPECT_TRUE(controller_factory->AttachToController(*bus, ID, device1)); + auto device2 = device_factory.CreateDevice(SCHS, LUN2, ""); + EXPECT_TRUE(controller_factory->AttachToController(*bus, ID, device2)); + + auto d1 = controller_factory->GetDeviceForIdAndLun(ID, LUN1); + EXPECT_FALSE(executor->Detach(context, *d1, false)) << "LUNs > 0 have to be detached first"; + auto d2 = controller_factory->GetDeviceForIdAndLun(ID, LUN2); + EXPECT_TRUE(executor->Detach(context, *d2, false)); + EXPECT_TRUE(executor->Detach(context, *d1, false)); + EXPECT_TRUE(controller_factory->GetAllDevices().empty()); + + EXPECT_FALSE(executor->Detach(context, *d1, false)); +} + +TEST(S2pExecutorTest, DetachAll) +{ + const int ID = 4; + + DeviceFactory device_factory; + auto bus = make_shared(); + auto controller_factory = make_shared(); + auto executor = make_shared(*bus, controller_factory); + + auto device = device_factory.CreateDevice(SCHS, 0, ""); + EXPECT_TRUE(controller_factory->AttachToController(*bus, ID, device)); + EXPECT_TRUE(controller_factory->HasController(ID)); + EXPECT_FALSE(controller_factory->GetAllDevices().empty()); + + executor->DetachAll(); + EXPECT_EQ(nullptr, controller_factory->FindController(ID)); + EXPECT_TRUE(controller_factory->GetAllDevices().empty()); +} + +TEST(S2pExecutorTest, SetReservedIds) +{ + DeviceFactory device_factory; + auto bus = make_shared(); + auto controller_factory = make_shared(); + auto executor = make_shared(*bus, controller_factory); + + string error = executor->SetReservedIds("xyz"); + EXPECT_FALSE(error.empty()); + EXPECT_TRUE(executor->GetReservedIds().empty()); + + error = executor->SetReservedIds("8"); + EXPECT_FALSE(error.empty()); + EXPECT_TRUE(executor->GetReservedIds().empty()); + + error = executor->SetReservedIds("-1"); + EXPECT_FALSE(error.empty()); + EXPECT_TRUE(executor->GetReservedIds().empty()); + + error = executor->SetReservedIds(""); + EXPECT_TRUE(error.empty()); + EXPECT_TRUE(executor->GetReservedIds().empty()); + + error = executor->SetReservedIds("7,1,2,3,5"); + EXPECT_TRUE(error.empty()); + unordered_set reserved_ids = executor->GetReservedIds(); + EXPECT_EQ(5, reserved_ids.size()); + EXPECT_TRUE(reserved_ids.contains(1)); + EXPECT_TRUE(reserved_ids.contains(2)); + EXPECT_TRUE(reserved_ids.contains(3)); + EXPECT_TRUE(reserved_ids.contains(5)); + EXPECT_TRUE(reserved_ids.contains(7)); + + auto device = device_factory.CreateDevice(SCHS, 0, ""); + EXPECT_TRUE(controller_factory->AttachToController(*bus, 5, device)); + error = executor->SetReservedIds("5"); + EXPECT_FALSE(error.empty()); +} + +TEST(S2pExecutorTest, ValidateImageFile) +{ + DeviceFactory device_factory; + auto bus = make_shared(); + auto controller_factory = make_shared(); + auto executor = make_shared(*bus, controller_factory); + PbCommand command; + CommandContext context(command, "", ""); + + auto device = dynamic_pointer_cast(device_factory.CreateDevice(SCHD, 0, "test")); + EXPECT_TRUE(executor->ValidateImageFile(context, *device, "")); + + EXPECT_FALSE(executor->ValidateImageFile(context, *device, "/non_existing_file")); +} + +TEST(S2pExecutorTest, PrintCommand) +{ + auto bus = make_shared(); + auto controller_factory = make_shared(); + auto executor = make_shared(*bus, controller_factory); + PbDeviceDefinition definition; + + PbCommand command; + string s = executor->PrintCommand(command, definition); + EXPECT_NE(s.find("operation="), string::npos); + EXPECT_EQ(s.find("key1=value1"), string::npos); + EXPECT_EQ(s.find("key2=value2"), string::npos); + + SetParam(command, "key1", "value1"); + s = executor->PrintCommand(command, definition); + EXPECT_NE(s.find("operation="), string::npos); + EXPECT_NE(s.find("key1=value1"), string::npos); + + SetParam(command, "key2", "value2"); + s = executor->PrintCommand(command, definition); + EXPECT_NE(s.find("operation="), string::npos); + EXPECT_NE(s.find("key1=value1"), string::npos); + EXPECT_NE(s.find("key2=value2"), string::npos); +} + +TEST(S2pExecutorTest, EnsureLun0) +{ + DeviceFactory device_factory; + auto bus = make_shared(); + auto controller_factory = make_shared(); + auto executor = make_shared(*bus, controller_factory); + PbCommand command; + + auto device1 = command.add_devices(); + device1->set_unit(0); + string error = executor->EnsureLun0(command); + EXPECT_TRUE(error.empty()); + + device1->set_unit(1); + error = executor->EnsureLun0(command); + EXPECT_FALSE(error.empty()); + + auto device2 = device_factory.CreateDevice(SCHS, 0, ""); + EXPECT_TRUE(controller_factory->AttachToController(*bus, 0, device2)); + error = executor->EnsureLun0(command); + EXPECT_TRUE(error.empty()); +} + +TEST(S2pExecutorTest, VerifyExistingIdAndLun) +{ + const int ID = 1; + const int LUN1 = 0; + const int LUN2 = 3; + + DeviceFactory device_factory; + auto bus = make_shared(); + auto controller_factory = make_shared(); + auto executor = make_shared(*bus, controller_factory); + PbCommand command; + CommandContext context(command, "", ""); + + EXPECT_FALSE(executor->VerifyExistingIdAndLun(context, ID, LUN1)); + auto device = device_factory.CreateDevice(SCHS, LUN1, ""); + EXPECT_TRUE(controller_factory->AttachToController(*bus, ID, device)); + EXPECT_TRUE(executor->VerifyExistingIdAndLun(context, ID, LUN1)); + EXPECT_FALSE(executor->VerifyExistingIdAndLun(context, ID, LUN2)); +} + +TEST(S2pExecutorTest, CreateDevice) +{ + auto bus = make_shared(); + auto controller_factory = make_shared(); + auto executor = make_shared(*bus, controller_factory); + PbCommand command; + CommandContext context(command, "", ""); + + EXPECT_EQ(nullptr, executor->CreateDevice(context, UNDEFINED, 0, "")); +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + EXPECT_EQ(nullptr, executor->CreateDevice(context, SCBR, 0, "")); +#pragma GCC diagnostic pop + EXPECT_NE(nullptr, executor->CreateDevice(context, UNDEFINED, 0, "services")); + EXPECT_NE(nullptr, executor->CreateDevice(context, SCHS, 0, "")); +} + +TEST(S2pExecutorTest, SetSectorSize) +{ + auto bus = make_shared(); + auto controller_factory = make_shared(); + auto executor = make_shared(*bus, controller_factory); + PbCommand command; + CommandContext context(command, "", ""); + + unordered_set sizes; + auto hd = make_shared(sizes); + EXPECT_FALSE(executor->SetSectorSize(context, hd, 512)); + + sizes.insert(512); + hd = make_shared(sizes); + EXPECT_TRUE(executor->SetSectorSize(context, hd, 0)); + EXPECT_FALSE(executor->SetSectorSize(context, hd, 1)); + EXPECT_FALSE(executor->SetSectorSize(context, hd, 512)); + + sizes.insert(1024); + hd = make_shared(sizes); + EXPECT_TRUE(executor->SetSectorSize(context, hd, 512)); +} + +TEST(S2pExecutorTest, ValidateOperationAgainstDevice) +{ + auto bus = make_shared(); + auto controller_factory = make_shared(); + auto executor = make_shared(*bus, controller_factory); + PbCommand command; + CommandContext context(command, "", ""); + + auto device = make_shared(0); + + EXPECT_TRUE(executor->ValidateOperationAgainstDevice(context, *device, ATTACH)); + EXPECT_TRUE(executor->ValidateOperationAgainstDevice(context, *device, DETACH)); + + EXPECT_FALSE(executor->ValidateOperationAgainstDevice(context, *device, START)); + EXPECT_FALSE(executor->ValidateOperationAgainstDevice(context, *device, STOP)); + EXPECT_FALSE(executor->ValidateOperationAgainstDevice(context, *device, INSERT)); + EXPECT_FALSE(executor->ValidateOperationAgainstDevice(context, *device, EJECT)); + EXPECT_FALSE(executor->ValidateOperationAgainstDevice(context, *device, PROTECT)); + EXPECT_FALSE(executor->ValidateOperationAgainstDevice(context, *device, UNPROTECT)); + + device->SetStoppable(true); + EXPECT_TRUE(executor->ValidateOperationAgainstDevice(context, *device, START)); + EXPECT_TRUE(executor->ValidateOperationAgainstDevice(context, *device, STOP)); + EXPECT_FALSE(executor->ValidateOperationAgainstDevice(context, *device, INSERT)); + EXPECT_FALSE(executor->ValidateOperationAgainstDevice(context, *device, EJECT)); + EXPECT_FALSE(executor->ValidateOperationAgainstDevice(context, *device, PROTECT)); + EXPECT_FALSE(executor->ValidateOperationAgainstDevice(context, *device, UNPROTECT)); + + device->SetRemovable(true); + EXPECT_TRUE(executor->ValidateOperationAgainstDevice(context, *device, START)); + EXPECT_TRUE(executor->ValidateOperationAgainstDevice(context, *device, STOP)); + EXPECT_TRUE(executor->ValidateOperationAgainstDevice(context, *device, INSERT)); + EXPECT_TRUE(executor->ValidateOperationAgainstDevice(context, *device, EJECT)); + EXPECT_FALSE(executor->ValidateOperationAgainstDevice(context, *device, PROTECT)); + EXPECT_FALSE(executor->ValidateOperationAgainstDevice(context, *device, UNPROTECT)); + + device->SetProtectable(true); + EXPECT_TRUE(executor->ValidateOperationAgainstDevice(context, *device, START)); + EXPECT_TRUE(executor->ValidateOperationAgainstDevice(context, *device, STOP)); + EXPECT_TRUE(executor->ValidateOperationAgainstDevice(context, *device, INSERT)); + EXPECT_TRUE(executor->ValidateOperationAgainstDevice(context, *device, EJECT)); + EXPECT_FALSE(executor->ValidateOperationAgainstDevice(context, *device, PROTECT)); + EXPECT_FALSE(executor->ValidateOperationAgainstDevice(context, *device, UNPROTECT)); + + device->SetReady(true); + EXPECT_TRUE(executor->ValidateOperationAgainstDevice(context, *device, START)); + EXPECT_TRUE(executor->ValidateOperationAgainstDevice(context, *device, STOP)); + EXPECT_TRUE(executor->ValidateOperationAgainstDevice(context, *device, INSERT)); + EXPECT_TRUE(executor->ValidateOperationAgainstDevice(context, *device, EJECT)); + EXPECT_TRUE(executor->ValidateOperationAgainstDevice(context, *device, PROTECT)); + EXPECT_TRUE(executor->ValidateOperationAgainstDevice(context, *device, UNPROTECT)); +} + +TEST(S2pExecutorTest, ValidateIdAndLun) +{ + auto bus = make_shared(); + auto controller_factory = make_shared(); + auto executor = make_shared(*bus, controller_factory); + PbCommand command; + CommandContext context(command, "", ""); + + EXPECT_FALSE(executor->ValidateIdAndLun(context, -1, 0)); + EXPECT_FALSE(executor->ValidateIdAndLun(context, 8, 0)); + EXPECT_FALSE(executor->ValidateIdAndLun(context, 7, -1)); + EXPECT_FALSE(executor->ValidateIdAndLun(context, 7, 32)); + EXPECT_TRUE(executor->ValidateIdAndLun(context, 7, 0)); + EXPECT_TRUE(executor->ValidateIdAndLun(context, 7, 31)); +} + +TEST(S2pExecutorTest, SetProductData) +{ + auto bus = make_shared(); + auto controller_factory = make_shared(); + auto executor = make_shared(*bus, controller_factory); + PbCommand command; + CommandContext context(command, "", ""); + PbDeviceDefinition definition; + + auto device = make_shared(0); + + EXPECT_TRUE(executor->SetProductData(context, definition, *device)); + + definition.set_vendor("123456789"); + EXPECT_FALSE(executor->SetProductData(context, definition, *device)); + definition.set_vendor("1"); + EXPECT_TRUE(executor->SetProductData(context, definition, *device)); + definition.set_vendor("12345678"); + EXPECT_TRUE(executor->SetProductData(context, definition, *device)); + + definition.set_product("12345678901234567"); + EXPECT_FALSE(executor->SetProductData(context, definition, *device)); + definition.set_product("1"); + EXPECT_TRUE(executor->SetProductData(context, definition, *device)); + definition.set_product("1234567890123456"); + EXPECT_TRUE(executor->SetProductData(context, definition, *device)); + + definition.set_revision("12345"); + EXPECT_FALSE(executor->SetProductData(context, definition, *device)); + definition.set_revision("1"); + EXPECT_TRUE(executor->SetProductData(context, definition, *device)); + definition.set_revision("1234"); + EXPECT_TRUE(executor->SetProductData(context, definition, *device)); +} diff --git a/cpp/test/s2p_image_test.cpp b/cpp/test/s2p_image_test.cpp new file mode 100644 index 00000000..5b34798e --- /dev/null +++ b/cpp/test/s2p_image_test.cpp @@ -0,0 +1,162 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" +#include "shared/protobuf_util.h" +#include "generated/command_interface.pb.h" +#include "s2p/s2p_image.h" + +using namespace command_interface; +using namespace protobuf_util; + +TEST(S2pImageTest, SetGetDepth) +{ + S2pImage image; + + image.SetDepth(1); + EXPECT_EQ(1, image.GetDepth()); +} + +TEST(S2pImageTest, SetGetDefaultFolder) +{ + S2pImage image; + + EXPECT_NE(string::npos, image.GetDefaultFolder().find("/images")); + + EXPECT_TRUE(!image.SetDefaultFolder("").empty()); + EXPECT_TRUE(!image.SetDefaultFolder("/not_in_home").empty()); +} + +TEST(S2pImageTest, CreateImage) +{ + S2pImage image; + + StorageDevice::UnreserveAll(); + + PbCommand command1; + CommandContext context1(command1, "", ""); + EXPECT_FALSE(image.CreateImage(context1)) << "Filename must be reported as missing"; + + PbCommand command2; + SetParam(command2, "file", "/a/b/c/filename"); + CommandContext context2(command2, "", ""); + EXPECT_FALSE(image.CreateImage(context2)) << "Depth must be reported as invalid"; + + PbCommand command3; + SetParam(command3, "file", "filename"); + SetParam(command3, "size", "-1"); + CommandContext context3(command3, "", ""); + EXPECT_FALSE(image.CreateImage(context3)) << "Size must be reported as invalid"; + + PbCommand command4; + SetParam(command4, "size", "1"); + CommandContext context4(command4, "", ""); + EXPECT_FALSE(image.CreateImage(context4)) << "Size must be reported as invalid"; + + PbCommand command5; + SetParam(command5, "size", "513"); + CommandContext context5(command5, "", ""); + EXPECT_FALSE(image.CreateImage(context4)) << "Size must be reported as not a multiple of 512"; + + // Further tests would modify the filesystem +} + +TEST(S2pImageTest, DeleteImage) +{ + S2pImage image; + + StorageDevice::UnreserveAll(); + + PbCommand command1; + CommandContext context1(command1, "", ""); + EXPECT_FALSE(image.DeleteImage(context1)) << "Filename must be reported as missing"; + + PbCommand command2; + SetParam(command2, "file", "/a/b/c/filename"); + CommandContext context2(command2, "", ""); + EXPECT_FALSE(image.DeleteImage(context2)) << "Depth must be reported as invalid"; + + MockStorageDevice device; + device.SetFilename("filename"); + device.ReserveFile(); + PbCommand command3; + SetParam(command3, "file", "filename"); + CommandContext context3(command3, "", ""); + EXPECT_FALSE(image.DeleteImage(context3)) << "File must be reported as in use"; + + // Further testing would modify the filesystem +} + +TEST(S2pImageTest, RenameImage) +{ + S2pImage image; + + StorageDevice::UnreserveAll(); + + PbCommand command1; + CommandContext context1(command1, "", ""); + EXPECT_FALSE(image.RenameImage(context1)) << "Source filename must be reported as missing"; + + PbCommand command2; + SetParam(command2, "from", "/a/b/c/filename_from"); + CommandContext context2(command2, "", ""); + EXPECT_FALSE(image.RenameImage(context2)) << "Depth must be reported as invalid"; + + PbCommand command3; + SetParam(command3, "from", "filename_from"); + CommandContext context3(command3, "", ""); + EXPECT_FALSE(image.RenameImage(context3)) << "Source file must be reported as missing"; + + // Further testing would modify the filesystem +} + +TEST(S2pImageTest, CopyImage) +{ + S2pImage image; + + StorageDevice::UnreserveAll(); + + PbCommand command1; + CommandContext context1(command1, "", ""); + EXPECT_FALSE(image.CopyImage(context1)) << "Source filename must be reported as missing"; + + PbCommand command2; + SetParam(command2, "from", "/a/b/c/filename_from"); + CommandContext context2(command2, "", ""); + EXPECT_FALSE(image.CopyImage(context2)) << "Depth must be reported as invalid"; + + PbCommand command3; + SetParam(command3, "from", "filename_from"); + CommandContext context3(command3, "", ""); + EXPECT_FALSE(image.CopyImage(context3)) << "Source file must be reported as missing"; + + // Further testing would modify the filesystem +} + +TEST(S2pImageTest, SetImagePermissions) +{ + S2pImage image; + + StorageDevice::UnreserveAll(); + + PbCommand command1; + CommandContext context1(command1, "", ""); + EXPECT_FALSE(image.SetImagePermissions(context1)) << "Filename must be reported as missing"; + + PbCommand command2; + SetParam(command2, "file", "/a/b/c/filename"); + CommandContext context2(command2, "", ""); + EXPECT_FALSE(image.SetImagePermissions(context2)) << "Depth must be reported as invalid"; + + PbCommand command3; + SetParam(command3, "file", "filename"); + CommandContext context3(command3, "", ""); + EXPECT_FALSE(image.CopyImage(context3)) << "Source file must be reported as missing"; + + // Further testing would modify the filesystem +} diff --git a/cpp/test/s2p_response_test.cpp b/cpp/test/s2p_response_test.cpp new file mode 100644 index 00000000..1417c1cf --- /dev/null +++ b/cpp/test/s2p_response_test.cpp @@ -0,0 +1,258 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" +#include "shared/s2p_version.h" +#include "shared/protobuf_util.h" +#include "controllers/controller_factory.h" +#include "base/device_factory.h" +#include "generated/command_interface.pb.h" +#include "s2p/s2p_response.h" + +using namespace command_interface; +using namespace spdlog; +using namespace protobuf_util; + +TEST(S2pResponseTest, Operation_Count) +{ + S2pResponse response; + + PbOperationInfo info; + response.GetOperationInfo(info, 0); + EXPECT_EQ(PbOperation_ARRAYSIZE - 1, info.operations_size()); +} + +void TestNonDiskDevice(PbDeviceType type, int default_param_count) +{ + auto bus = make_shared(); + ControllerFactory controller_factory; + DeviceFactory device_factory; + S2pResponse response; + + auto d = device_factory.CreateDevice(type, 0, ""); + const param_map params; + d->Init(params); + EXPECT_TRUE(controller_factory.AttachToController(*bus, 0, d)); + + PbServerInfo info; + response.GetDevices(controller_factory.GetAllDevices(), info, "image_folder"); + + EXPECT_EQ(1, info.devices_info().devices().size()); + + const auto& device = info.devices_info().devices()[0]; + EXPECT_FALSE(device.properties().read_only()); + EXPECT_FALSE(device.properties().protectable()); + EXPECT_FALSE(device.properties().stoppable()); + EXPECT_FALSE(device.properties().removable()); + EXPECT_FALSE(device.properties().lockable()); + EXPECT_EQ(32, device.properties().luns()); + EXPECT_EQ(0, device.block_size()); + EXPECT_EQ(0, device.block_count()); + EXPECT_EQ(default_param_count, device.properties().default_params().size()); + EXPECT_FALSE(device.properties().supports_file()); + if (default_param_count) { + EXPECT_TRUE(device.properties().supports_params()); + } + else { + EXPECT_FALSE(device.properties().supports_params()); + } +} + +TEST(S2pResponseTest, GetDevices) +{ + TestNonDiskDevice(SCHS, 0); + TestNonDiskDevice(SCLP, 1); +} + +TEST(S2pResponseTest, GetImageFile) +{ + S2pResponse response; + PbImageFile image_file; + + EXPECT_FALSE(response.GetImageFile(image_file, "default_folder", "")); + + // Even though the call fails (non-existing file) some properties must be set + EXPECT_FALSE(response.GetImageFile(image_file, "default_folder", "filename.hds")); + EXPECT_EQ("filename.hds", image_file.name()); + EXPECT_EQ(SCHD, image_file.type()); +} + +TEST(S2pResponseTest, GetImageFilesInfo) +{ + S2pResponse response; + + PbImageFilesInfo info; + response.GetImageFilesInfo(info, "default_folder", "", "", 1); + EXPECT_TRUE(info.image_files().empty()); +} + +TEST(S2pResponseTest, GetReservedIds) +{ + S2pResponse response; + unordered_set ids; + + PbReservedIdsInfo info1; + response.GetReservedIds(info1, ids); + EXPECT_TRUE(info1.ids().empty()); + + ids.insert(3); + PbReservedIdsInfo info2; + response.GetReservedIds(info2, ids); + EXPECT_EQ(1, info2.ids().size()); + EXPECT_EQ(3, info2.ids()[0]); +} + +TEST(S2pResponseTest, GetDevicesInfo) +{ + const int ID = 2; + const int LUN1 = 0; + const int LUN2 = 5; + const int LUN3 = 6; + + auto bus = make_shared(); + ControllerFactory controller_factory; + S2pResponse response; + PbCommand command; + + PbResult result1; + response.GetDevicesInfo(controller_factory.GetAllDevices(), result1, command, ""); + EXPECT_TRUE(result1.status()); + EXPECT_TRUE(result1.devices_info().devices().empty()); + + auto device1 = make_shared(LUN1); + EXPECT_TRUE(controller_factory.AttachToController(*bus, ID, device1)); + + response.GetDevicesInfo(controller_factory.GetAllDevices(), result1, command, ""); + EXPECT_TRUE(result1.status()); + auto& devices1 = result1.devices_info().devices(); + EXPECT_EQ(1, devices1.size()); + EXPECT_EQ(SCHS, devices1[0].type()); + EXPECT_EQ(ID, devices1[0].id()); + EXPECT_EQ(LUN1, devices1[0].unit()); + + auto device2 = make_shared(LUN2, false); + EXPECT_TRUE(controller_factory.AttachToController(*bus, ID, device2)); + + PbResult result2; + response.GetDevicesInfo(controller_factory.GetAllDevices(), result2, command, ""); + EXPECT_TRUE(result2.status()); + auto& devices2 = result2.devices_info().devices(); + EXPECT_EQ(2, devices2.size()) << "Device count mismatch"; + + auto requested_device = command.add_devices(); + requested_device->set_id(ID); + requested_device->set_unit(LUN1); + PbResult result3; + response.GetDevicesInfo(controller_factory.GetAllDevices(), result3, command, ""); + EXPECT_TRUE(result3.status()); + auto& devices3 = result3.devices_info().devices(); + EXPECT_EQ(1, devices3.size()) << "Only data for the specified ID and LUN must be returned"; + EXPECT_EQ(SCHS, devices3[0].type()); + EXPECT_EQ(ID, devices3[0].id()); + EXPECT_EQ(LUN1, devices3[0].unit()); + + requested_device->set_id(ID); + requested_device->set_unit(LUN3); + PbResult result4; + response.GetDevicesInfo(controller_factory.GetAllDevices(), result4, command, ""); + EXPECT_FALSE(result4.status()) << "Only data for the specified ID and LUN must be returned"; +} + +TEST(S2pResponseTest, GetDeviceTypesInfo) +{ + S2pResponse response; + + PbDeviceTypesInfo info; + response.GetDeviceTypesInfo(info); + EXPECT_EQ(8, info.properties().size()); +} + +TEST(S2pResponseTest, GetServerInfo) +{ + auto bus = make_shared(); + S2pResponse response; + const unordered_set> devices; + const unordered_set ids = { 1, 3 }; + + PbCommand command; + PbServerInfo info1; + response.GetServerInfo(info1, command, devices, ids, "default_folder", 1234); + EXPECT_TRUE(info1.has_version_info()); + EXPECT_TRUE(info1.has_log_level_info()); + EXPECT_TRUE(info1.has_device_types_info()); + EXPECT_TRUE(info1.has_image_files_info()); + EXPECT_TRUE(info1.has_network_interfaces_info()); + EXPECT_TRUE(info1.has_mapping_info()); + EXPECT_TRUE(info1.has_statistics_info()); + EXPECT_FALSE(info1.has_devices_info()); + EXPECT_TRUE(info1.has_reserved_ids_info()); + EXPECT_TRUE(info1.has_operation_info()); + + EXPECT_EQ(s2p_major_version, info1.version_info().major_version()); + EXPECT_EQ(s2p_minor_version, info1.version_info().minor_version()); + EXPECT_EQ(s2p_patch_version, info1.version_info().patch_version()); + EXPECT_EQ(level::level_string_views[get_level()], info1.log_level_info().current_log_level()); + EXPECT_EQ("default_folder", info1.image_files_info().default_image_folder()); + EXPECT_EQ(1234, info1.image_files_info().depth()); + EXPECT_EQ(2, info1.reserved_ids_info().ids().size()); + + SetParam(command, "operations", "log_level_info,mapping_info"); + PbServerInfo info2; + response.GetServerInfo(info2, command, devices, ids, "default_folder", 1234); + EXPECT_FALSE(info2.has_version_info()); + EXPECT_TRUE(info2.has_log_level_info()); + EXPECT_FALSE(info2.has_device_types_info()); + EXPECT_FALSE(info2.has_image_files_info()); + EXPECT_FALSE(info2.has_network_interfaces_info()); + EXPECT_TRUE(info2.has_mapping_info()); + EXPECT_FALSE(info2.has_statistics_info()); + EXPECT_FALSE(info2.has_devices_info()); + EXPECT_FALSE(info2.has_reserved_ids_info()); + EXPECT_FALSE(info2.has_operation_info()); +} + +TEST(S2pResponseTest, GetVersionInfo) +{ + S2pResponse response; + + PbVersionInfo info; + response.GetVersionInfo(info); + EXPECT_EQ(s2p_major_version, info.major_version()); + EXPECT_EQ(s2p_minor_version, info.minor_version()); + EXPECT_EQ(s2p_patch_version, info.patch_version()); +} + +TEST(S2pResponseTest, GetLogLevelInfo) +{ + S2pResponse response; + + PbLogLevelInfo info; + response.GetLogLevelInfo(info); + EXPECT_EQ(level::level_string_views[get_level()], info.current_log_level()); + EXPECT_EQ(7, info.log_levels().size()); +} + +#ifdef __linux__ +TEST(S2pResponseTest, GetNetworkInterfacesInfo) +{ + S2pResponse response; + + PbNetworkInterfacesInfo info; + response.GetNetworkInterfacesInfo(info); + EXPECT_FALSE(info.name().empty()); +} +#endif + +TEST(S2pResponseTest, GetMappingInfo) +{ + S2pResponse response; + + PbMappingInfo info; + response.GetMappingInfo(info); + EXPECT_EQ(7, info.mapping().size()); +} diff --git a/cpp/test/s2p_service_test.cpp b/cpp/test/s2p_service_test.cpp new file mode 100644 index 00000000..26a055c4 --- /dev/null +++ b/cpp/test/s2p_service_test.cpp @@ -0,0 +1,109 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +// These tests only test up the point where a network connection is required. +// +//--------------------------------------------------------------------------- + +#include + +#include "generated/command_interface.pb.h" +#include "shared/protobuf_util.h" +#include "shared/network_util.h" +#include "shared/shared_exceptions.h" +#include "s2p/command_context.h" +#include "s2p/s2p_service.h" +#include +#include +#include +#include +#include + +using namespace command_interface; +using namespace protobuf_util; +using namespace network_util; + +void SendCommand(const PbCommand& command, PbResult& result) +{ + sockaddr_in server_addr = {}; + ASSERT_TRUE(ResolveHostName("127.0.0.1", &server_addr)); + server_addr.sin_port = htons(uint16_t(9999)); + + const int fd = socket(AF_INET, SOCK_STREAM, 0); + ASSERT_NE(-1, fd); + EXPECT_TRUE(connect(fd, reinterpret_cast(&server_addr), sizeof(server_addr)) >= 0) << "Service should be running"; //NOSONAR bit_cast is not supported by the bullseye clang++ compiler + ASSERT_EQ(6, write(fd, "RASCSI", 6)); + SerializeMessage(fd, command); + DeserializeMessage(fd, result); + close(fd); +} + +TEST(S2pServiceTest, Init) +{ + S2pService service; + + EXPECT_FALSE(service.Init(nullptr, 65536).empty()) << "Illegal port number"; + EXPECT_FALSE(service.Init(nullptr, 0).empty()) << "Illegal port number"; + EXPECT_FALSE(service.Init(nullptr, -1).empty()) << "Illegal port number"; + EXPECT_FALSE(service.Init(nullptr, 1).empty()) << "Port 1 is only available for the root user"; + EXPECT_TRUE(service.Init(nullptr, 9999).empty()) << "Port 9999 is expected not to be in use for this test"; + service.Stop(); +} + +TEST(S2pServiceTest, IsRunning) +{ + S2pService service; + EXPECT_FALSE(service.IsRunning()); + EXPECT_TRUE(service.Init(nullptr, 9999).empty()) << "Port 9999 is expected not to be in use for this test"; + EXPECT_FALSE(service.IsRunning()); + + service.Start(); + EXPECT_TRUE(service.IsRunning()); + service.Stop(); + EXPECT_FALSE(service.IsRunning()); +} + +TEST(S2pServiceTest, Execute) +{ + sockaddr_in server_addr = {}; + ASSERT_TRUE(ResolveHostName("127.0.0.1", &server_addr)); + + const int fd = socket(AF_INET, SOCK_STREAM, 0); + ASSERT_NE(-1, fd); + + server_addr.sin_port = htons(uint16_t(9999)); + EXPECT_FALSE(connect(fd, reinterpret_cast(&server_addr), sizeof(server_addr)) >= 0) << "Service should not be running"; //NOSONAR bit_cast is not supported by the bullseye clang++ compiler + + close(fd); + + S2pService service; + service.Init([] (const CommandContext& context) { + if (context.GetCommand().operation() == PbOperation::NO_OPERATION) { + PbResult result; + result.set_status(true); + context.WriteResult(result); + } + else { + throw io_exception("error"); + } + return true; + }, 9999); + + service.Start(); + + PbCommand command; + PbResult result; + + SendCommand(command, result); + command.set_operation(PbOperation::NO_OPERATION); + EXPECT_TRUE(result.status()) << "Command should have been successful"; + + command.set_operation(PbOperation::EJECT); + SendCommand(command, result); + EXPECT_FALSE(result.status()) << "Exception should have been raised"; + + service.Stop(); +} diff --git a/cpp/test/s2p_util_test.cpp b/cpp/test/s2p_util_test.cpp new file mode 100644 index 00000000..316d618d --- /dev/null +++ b/cpp/test/s2p_util_test.cpp @@ -0,0 +1,138 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include + +#include "shared/s2p_util.h" + +using namespace std; +using namespace s2p_util; + +TEST(S2pUtilTest, Split) +{ + auto v = Split("this_is_a_test", '_'); + EXPECT_EQ(4, v.size()); + EXPECT_EQ("this", v[0]); + EXPECT_EQ("is", v[1]); + EXPECT_EQ("a", v[2]); + EXPECT_EQ("test", v[3]); + v = Split("test", ':'); + EXPECT_EQ(1, v.size()); + EXPECT_EQ("test", v[0]); + v = Split(":test", ':'); + EXPECT_EQ(2, v.size()); + EXPECT_EQ("", v[0]); + EXPECT_EQ("test", v[1]); + v = Split("test:", ':'); + EXPECT_EQ(1, v.size()); + EXPECT_EQ("test", v[0]); + v = Split(":", ':'); + EXPECT_EQ(1, v.size()); + EXPECT_EQ("", v[0]); + v = Split("", ':'); + EXPECT_EQ(0, v.size()); + + v = Split("this:is:a:test", ':', 1); + EXPECT_EQ(1, v.size()); + EXPECT_EQ("this:is:a:test", v[0]); + v = Split("this:is:a:test", ':', 2); + EXPECT_EQ(2, v.size()); + EXPECT_EQ("this", v[0]); + EXPECT_EQ("is:a:test", v[1]); +} + +TEST(S2pUtilTest, GetLocale) +{ + EXPECT_LE(2, GetLocale().size()); +} + +TEST(S2pUtilTest, ProcessId) +{ + int id = -1; + int lun = -1; + + string error = ProcessId(8, 32, "", id, lun); + EXPECT_FALSE(error.empty()); + EXPECT_EQ(-1, id); + EXPECT_EQ(-1, lun); + + error = ProcessId(8, 32, "8", id, lun); + EXPECT_FALSE(error.empty()); + EXPECT_EQ(-1, id); + EXPECT_EQ(-1, lun); + + error = ProcessId(8, 32, "0:32", id, lun); + EXPECT_FALSE(error.empty()); + EXPECT_EQ(-1, id); + EXPECT_EQ(-1, lun); + + error = ProcessId(8, 32, "-1:", id, lun); + EXPECT_FALSE(error.empty()); + EXPECT_EQ(-1, id); + EXPECT_EQ(-1, lun); + + error = ProcessId(8, 32, "0:-1", id, lun); + EXPECT_FALSE(error.empty()); + EXPECT_EQ(-1, id); + EXPECT_EQ(-1, lun); + + error = ProcessId(8, 32, "a", id, lun); + EXPECT_FALSE(error.empty()); + EXPECT_EQ(-1, id); + EXPECT_EQ(-1, lun); + + error = ProcessId(8, 32, "a:0", id, lun); + EXPECT_FALSE(error.empty()); + EXPECT_EQ(-1, id); + EXPECT_EQ(-1, lun); + + error = ProcessId(8, 32, "0:a", id, lun); + EXPECT_FALSE(error.empty()); + EXPECT_EQ(-1, id); + EXPECT_EQ(-1, lun); + + error = ProcessId(8, 32, "0", id, lun); + EXPECT_TRUE(error.empty()); + EXPECT_EQ(0, id); + EXPECT_EQ(-1, lun); + + error = ProcessId(8, 32, "7:31", id, lun); + EXPECT_TRUE(error.empty()); + EXPECT_EQ(7, id); + EXPECT_EQ(31, lun); +} + +TEST(S2pUtilTest, GetAsUnsignedInt) +{ + int result; + + EXPECT_FALSE(GetAsUnsignedInt("", result)); + EXPECT_FALSE(GetAsUnsignedInt("xyz", result)); + EXPECT_FALSE(GetAsUnsignedInt("-1", result)); + EXPECT_FALSE(GetAsUnsignedInt("1234567898765432112345678987654321", result)) << "Value is out of range"; + EXPECT_TRUE(GetAsUnsignedInt("0", result)); + EXPECT_EQ(0, result); + EXPECT_TRUE(GetAsUnsignedInt("1234", result)); + EXPECT_EQ(1234, result); +} + +TEST(S2pUtilTest, Banner) +{ + EXPECT_FALSE(Banner("Test").empty()); +} + +TEST(S2pUtilTest, GetExtensionLowerCase) +{ + EXPECT_EQ("", GetExtensionLowerCase("")); + EXPECT_EQ("", GetExtensionLowerCase(".")); + EXPECT_EQ("", GetExtensionLowerCase(".ext")); + EXPECT_EQ("", GetExtensionLowerCase(".ext_long")); + EXPECT_EQ("ext", GetExtensionLowerCase("file.ext")); + EXPECT_EQ("ext", GetExtensionLowerCase("FILE.EXT")); + EXPECT_EQ("ext", GetExtensionLowerCase(".XYZ.EXT")); +} diff --git a/cpp/test/s2pctl_commands_test.cpp b/cpp/test/s2pctl_commands_test.cpp new file mode 100644 index 00000000..7d3a4ad8 --- /dev/null +++ b/cpp/test/s2pctl_commands_test.cpp @@ -0,0 +1,110 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +// These tests only test up the point where a network connection is required. +// +//--------------------------------------------------------------------------- + +#include + +#include "shared/protobuf_util.h" +#include "shared/shared_exceptions.h" +#include "generated/command_interface.pb.h" +#include "s2pctl/s2pctl_commands.h" + +using namespace testing; +using namespace command_interface; +using namespace protobuf_util; + +TEST(S2pCtlCommandsTest, Execute) +{ + PbCommand command; + S2pCtlCommands commands(command, "localhost", 0, "", "", ""); + + command.set_operation(LOG_LEVEL); + EXPECT_THROW(commands.Execute("log_level", "", "", "", ""), io_exception); + EXPECT_EQ("log_level", GetParam(command, "level")); + + command.set_operation(DEFAULT_FOLDER); + EXPECT_THROW(commands.Execute("", "default_folder", "", "", ""), io_exception); + EXPECT_EQ("default_folder", GetParam(command, "folder")); + + command.set_operation(RESERVE_IDS); + EXPECT_THROW(commands.Execute("", "", "reserved_ids", "", ""), io_exception); + EXPECT_EQ("reserved_ids", GetParam(command, "ids")); + + command.set_operation(CREATE_IMAGE); + EXPECT_FALSE(commands.Execute("", "", "", "", "")); + EXPECT_THROW(commands.Execute("", "", "", "filename:0", ""), io_exception); + EXPECT_EQ("false", GetParam(command, "read_only")); + + command.set_operation(DELETE_IMAGE); + EXPECT_THROW(commands.Execute("", "", "", "filename1", ""), io_exception); + EXPECT_EQ("filename1", GetParam(command, "file")); + + command.set_operation(RENAME_IMAGE); + EXPECT_FALSE(commands.Execute("", "", "", "", "")); + EXPECT_THROW(commands.Execute("", "", "", "from1:to1", ""), io_exception); + EXPECT_EQ("from1", GetParam(command, "from")); + EXPECT_EQ("to1", GetParam(command, "to")); + + command.set_operation(COPY_IMAGE); + EXPECT_FALSE(commands.Execute("", "", "", "", "")); + EXPECT_THROW(commands.Execute("", "", "", "from2:to2", ""), io_exception); + EXPECT_EQ("from2", GetParam(command, "from")); + EXPECT_EQ("to2", GetParam(command, "to")); + + command.set_operation(DEVICES_INFO); + EXPECT_THROW(commands.Execute("", "", "", "", ""), io_exception); + + command.set_operation(DEVICE_TYPES_INFO); + EXPECT_THROW(commands.Execute("", "", "", "", ""), io_exception); + + command.set_operation(VERSION_INFO); + EXPECT_THROW(commands.Execute("", "", "", "", ""), io_exception); + + command.set_operation(SERVER_INFO); + EXPECT_THROW(commands.Execute("", "", "", "", ""), io_exception); + + command.set_operation(DEFAULT_IMAGE_FILES_INFO); + EXPECT_THROW(commands.Execute("", "", "", "", ""), io_exception); + + command.set_operation(IMAGE_FILE_INFO); + EXPECT_THROW(commands.Execute("", "", "", "", "filename2"), io_exception); + EXPECT_EQ("filename2", GetParam(command, "file")); + + command.set_operation(NETWORK_INTERFACES_INFO); + EXPECT_THROW(commands.Execute("", "", "", "", ""), io_exception); + + command.set_operation(LOG_LEVEL_INFO); + EXPECT_THROW(commands.Execute("", "", "", "", ""), io_exception); + + command.set_operation(RESERVED_IDS_INFO); + EXPECT_THROW(commands.Execute("", "", "", "", ""), io_exception); + + command.set_operation(MAPPING_INFO); + EXPECT_THROW(commands.Execute("", "", "", "", ""), io_exception); + + command.set_operation(OPERATION_INFO); + EXPECT_THROW(commands.Execute("", "", "", "", ""), io_exception); + + command.set_operation(DETACH_ALL); + EXPECT_THROW(commands.Execute("", "", "", "", ""), io_exception); + + command.set_operation(NO_OPERATION); + EXPECT_FALSE(commands.Execute("", "", "", "", "")); +} + +TEST(S2pCtlCommandsTest, CommandDevicesInfo) +{ + PbCommand command; + + S2pCtlCommands commands1(command, "/invalid_host_name", 0, "", "", ""); + EXPECT_THROW(commands1.CommandDevicesInfo(), io_exception); + + S2pCtlCommands commands2(command, "localhost", 0, "", "", ""); + EXPECT_THROW(commands2.CommandDevicesInfo(), io_exception); +} diff --git a/cpp/test/s2pctl_display_test.cpp b/cpp/test/s2pctl_display_test.cpp new file mode 100644 index 00000000..c39d5a04 --- /dev/null +++ b/cpp/test/s2pctl_display_test.cpp @@ -0,0 +1,292 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +// These tests only test key aspects of the expected output, because the output may change over time. +// +//--------------------------------------------------------------------------- + +#include + +#include "s2pctl/s2pctl_display.h" + +TEST(S2pCtlDisplayTest, DisplayDevicesInfo) +{ + S2pCtlDisplay display; + PbDevicesInfo info; + + EXPECT_FALSE(display.DisplayDevicesInfo(info).empty()); +} + +TEST(S2pCtlDisplayTest, DisplayDeviceInfo) +{ + S2pCtlDisplay display; + PbDevice device; + + EXPECT_FALSE(display.DisplayDeviceInfo(device).empty()); + + device.mutable_properties()->set_supports_file(true); + device.mutable_properties()->set_read_only(true); + device.mutable_properties()->set_protectable(true); + device.mutable_status()->set_protected_(true); + device.mutable_properties()->set_stoppable(true); + device.mutable_status()->set_stopped(true); + device.mutable_properties()->set_removable(true); + device.mutable_status()->set_removed(true); + device.mutable_properties()->set_lockable(true); + device.mutable_status()->set_locked(true); + EXPECT_FALSE(display.DisplayDeviceInfo(device).empty()); + + device.set_block_size(1234); + string s = display.DisplayDeviceInfo(device); + EXPECT_FALSE(s.empty()); + EXPECT_NE(string::npos, s.find("1234")); + + device.set_block_count(4321); + s = display.DisplayDeviceInfo(device); + EXPECT_FALSE(s.empty()); + EXPECT_NE(string::npos, s.find(to_string(1234 *4321))); + + device.mutable_properties()->set_supports_file(true); + auto file = device.mutable_file(); + file->set_name("filename"); + s = display.DisplayDeviceInfo(device); + EXPECT_FALSE(s.empty()); + EXPECT_NE(string::npos, s.find("filename")); + + device.mutable_properties()->set_supports_params(true); + (*device.mutable_params())["key1"] = "value1"; + s = display.DisplayDeviceInfo(device); + EXPECT_FALSE(s.empty()); + EXPECT_NE(string::npos, s.find("key1=value1")); + (*device.mutable_params())["key2"] = "value2"; + s = display.DisplayDeviceInfo(device); + EXPECT_FALSE(s.empty()); + EXPECT_NE(string::npos, s.find("key1=value1")); + EXPECT_NE(string::npos, s.find("key2=value2")); +} + +TEST(S2pCtlDisplayTest, DisplayVersionInfo) +{ + S2pCtlDisplay display; + PbVersionInfo info; + + info.set_major_version(1); + info.set_minor_version(2); + info.set_patch_version(3); + string s = display.DisplayVersionInfo(info); + EXPECT_FALSE(s.empty()); + EXPECT_EQ(string::npos, s.find("development version")); + EXPECT_NE(string::npos, s.find("1.2.3")); + + info.set_patch_version(-1); + s = display.DisplayVersionInfo(info); + EXPECT_FALSE(s.empty()); + EXPECT_NE(string::npos, s.find("development version")); + EXPECT_NE(string::npos, s.find("1.2")); +} + +TEST(S2pCtlDisplayTest, DisplayLogLevelInfo) +{ + S2pCtlDisplay display; + PbLogLevelInfo info; + + string s = display.DisplayLogLevelInfo(info); + EXPECT_FALSE(s.empty()); + + info.add_log_levels("test"); + s = display.DisplayLogLevelInfo(info); + EXPECT_FALSE(s.empty()); + EXPECT_NE(string::npos, s.find("test")); +} + +TEST(S2pCtlDisplayTest, DisplayDeviceTypesInfo) +{ + S2pCtlDisplay display; + PbDeviceTypesInfo info; + + // Start with 2 instead of 1. 1 was the removed SASI drive type. + int ordinal = 2; + while (PbDeviceType_IsValid(ordinal)) { + PbDeviceType type = UNDEFINED; + PbDeviceType_Parse(PbDeviceType_Name((PbDeviceType)ordinal), &type); + + auto type_properties = info.add_properties(); + type_properties->set_type(type); + + if (type == SCHD) { + type_properties->mutable_properties()->set_supports_file(true); + type_properties->mutable_properties()->add_block_sizes(512); + type_properties->mutable_properties()->add_block_sizes(1024); + } + + if (type == SCMO) { + type_properties->mutable_properties()->set_supports_file(true); + type_properties->mutable_properties()->set_read_only(true); + type_properties->mutable_properties()->set_protectable(true); + type_properties->mutable_properties()->set_stoppable(true); + type_properties->mutable_properties()->set_removable(true); + type_properties->mutable_properties()->set_lockable(true); + } + + if (type == SCLP) { + type_properties->mutable_properties()->set_supports_params(true); + (*type_properties->mutable_properties()->mutable_default_params())["key1"] = "value1"; + (*type_properties->mutable_properties()->mutable_default_params())["key2"] = "value2"; + } + + ordinal++; + } + + const string s = display.DisplayDeviceTypesInfo(info); + EXPECT_FALSE(s.empty()); + EXPECT_NE(string::npos, s.find("key1=value1")); + EXPECT_NE(string::npos, s.find("key2=value2")); +} + +TEST(S2pCtlDisplayTest, DisplayReservedIdsInfo) +{ + S2pCtlDisplay display; + PbReservedIdsInfo info; + + string s = display.DisplayReservedIdsInfo(info); + EXPECT_TRUE(s.empty()); + + info.mutable_ids()->Add(5); + s = display.DisplayReservedIdsInfo(info); + EXPECT_FALSE(s.empty()); + EXPECT_NE(string::npos, s.find("5")); + + info.mutable_ids()->Add(6); + s = display.DisplayReservedIdsInfo(info); + EXPECT_FALSE(s.empty()); + EXPECT_NE(string::npos, s.find("5, 6")); +} + +TEST(S2pCtlDisplayTest, DisplayNetworkInterfacesInfo) +{ + S2pCtlDisplay display; + PbNetworkInterfacesInfo info; + + string s = display.DisplayNetworkInterfaces(info); + EXPECT_FALSE(s.empty()); + + info.mutable_name()->Add("eth0"); + s = display.DisplayNetworkInterfaces(info); + EXPECT_FALSE(s.empty()); + EXPECT_NE(string::npos, s.find("eth0")); + + info.mutable_name()->Add("wlan0"); + s = display.DisplayNetworkInterfaces(info); + EXPECT_FALSE(s.empty()); + EXPECT_NE(string::npos, s.find("eth0, wlan0")); +} + +TEST(S2pCtlDisplayTest, DisplayImageFile) +{ + S2pCtlDisplay display; + PbImageFile file; + + string s = display.DisplayImageFile(file); + EXPECT_FALSE(s.empty()); + + file.set_name("filename"); + s = display.DisplayImageFile(file); + EXPECT_FALSE(s.empty()); + EXPECT_NE(string::npos, s.find("filename")); + EXPECT_EQ(string::npos, s.find("read-only")); + EXPECT_EQ(string::npos, s.find("SCHD")); + + file.set_read_only(true); + s = display.DisplayImageFile(file); + EXPECT_FALSE(s.empty()); + EXPECT_NE(string::npos, s.find("filename")); + EXPECT_NE(string::npos, s.find("read-only")); + EXPECT_EQ(string::npos, s.find("SCHD")); + + file.set_type(SCHD); + s = display.DisplayImageFile(file); + EXPECT_FALSE(s.empty()); + EXPECT_NE(string::npos, s.find("SCHD")); +} + +TEST(S2pCtlDisplayTest, DisplayImageFilesInfo) +{ + S2pCtlDisplay display; + PbImageFilesInfo info; + + string s = display.DisplayImageFilesInfo(info); + EXPECT_FALSE(display.DisplayImageFilesInfo(info).empty()); + EXPECT_EQ(string::npos, s.find("filename")); + + PbImageFile *file = info.add_image_files(); + file->set_name("filename"); + s = display.DisplayImageFilesInfo(info); + EXPECT_FALSE(s.empty()); + EXPECT_NE(string::npos, s.find("filename")); +} + +TEST(S2pCtlDisplayTest, DisplayMappingInfo) +{ + S2pCtlDisplay display; + PbMappingInfo info; + + string s = display.DisplayMappingInfo(info); + EXPECT_FALSE(s.empty()); + EXPECT_EQ(string::npos, s.find("key->SCHD")); + + (*info.mutable_mapping())["key"] = SCHD; + s = display.DisplayMappingInfo(info); + EXPECT_FALSE(s.empty()); + EXPECT_NE(string::npos, s.find("key->SCHD")); +} + +TEST(S2pCtlDisplayTest, DisplayOperationInfo) +{ + S2pCtlDisplay display; + PbOperationInfo info; + + string s = display.DisplayOperationInfo(info); + EXPECT_FALSE(s.empty()); + + PbOperationMetaData meta_data; + PbOperationParameter *param1 = meta_data.add_parameters(); + param1->set_name("default_key1"); + param1->set_default_value("default_value1"); + PbOperationParameter *param2 = meta_data.add_parameters(); + param2->set_name("default_key2"); + param2->set_default_value("default_value2"); + param2->set_description("description2"); + PbOperationParameter *param3 = meta_data.add_parameters(); + param3->set_name("default_key3"); + param3->set_default_value("default_value3"); + param3->set_description("description3"); + param3->add_permitted_values("permitted_value3_1"); + param3->add_permitted_values("permitted_value3_2"); + (*info.mutable_operations())[0] = meta_data; + s = display.DisplayOperationInfo(info); + EXPECT_FALSE(s.empty()); + EXPECT_NE(string::npos, s.find(PbOperation_Name(NO_OPERATION))); + + meta_data.set_server_side_name("server_side_name"); + meta_data.set_description("description"); + (*info.mutable_operations())[0] = meta_data; + s = display.DisplayOperationInfo(info); + EXPECT_FALSE(s.empty()); + EXPECT_NE(string::npos, s.find("default_key1")); + EXPECT_NE(string::npos, s.find("default_value1")); + EXPECT_NE(string::npos, s.find("default_key2")); + EXPECT_NE(string::npos, s.find("default_value2")); + EXPECT_NE(string::npos, s.find("description2")); + EXPECT_NE(string::npos, s.find("description3")); + EXPECT_NE(string::npos, s.find("permitted_value3_1")); + EXPECT_NE(string::npos, s.find("permitted_value3_2")); + EXPECT_EQ(string::npos, s.find("server_side_name")); + + (*info.mutable_operations())[1234] = meta_data; + s = display.DisplayOperationInfo(info); + EXPECT_FALSE(s.empty()); + EXPECT_NE(string::npos, s.find("server_side_name")); +} diff --git a/cpp/test/s2pctl_parser_test.cpp b/cpp/test/s2pctl_parser_test.cpp new file mode 100644 index 00000000..7c1a12e7 --- /dev/null +++ b/cpp/test/s2pctl_parser_test.cpp @@ -0,0 +1,50 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include + +#include "s2pctl/s2pctl_parser.h" + +TEST(S2pCtlParserTest, ParseOperation) +{ + S2pCtlParser parser; + + EXPECT_EQ(ATTACH, parser.ParseOperation("A")); + EXPECT_EQ(ATTACH, parser.ParseOperation("a")); + EXPECT_EQ(DETACH, parser.ParseOperation("d")); + EXPECT_EQ(INSERT, parser.ParseOperation("i")); + EXPECT_EQ(EJECT, parser.ParseOperation("e")); + EXPECT_EQ(PROTECT, parser.ParseOperation("p")); + EXPECT_EQ(UNPROTECT, parser.ParseOperation("u")); + EXPECT_EQ(NO_OPERATION, parser.ParseOperation("")); + EXPECT_EQ(NO_OPERATION, parser.ParseOperation("xyz")); +} + +TEST(S2pCtlParserTest, ParseType) +{ + S2pCtlParser parser; + + EXPECT_EQ(SCCD, parser.ParseType("sccd")); + EXPECT_EQ(SCDP, parser.ParseType("scdp")); + EXPECT_EQ(SCHD, parser.ParseType("schd")); + EXPECT_EQ(SCLP, parser.ParseType("sclp")); + EXPECT_EQ(SCMO, parser.ParseType("scmo")); + EXPECT_EQ(SCRM, parser.ParseType("scrm")); + EXPECT_EQ(SCHS, parser.ParseType("schs")); + + EXPECT_EQ(SCCD, parser.ParseType("c")); + EXPECT_EQ(SCDP, parser.ParseType("d")); + EXPECT_EQ(SCHD, parser.ParseType("h")); + EXPECT_EQ(SCLP, parser.ParseType("l")); + EXPECT_EQ(SCMO, parser.ParseType("m")); + EXPECT_EQ(SCRM, parser.ParseType("r")); + EXPECT_EQ(SCHS, parser.ParseType("s")); + + EXPECT_EQ(UNDEFINED, parser.ParseType("")); + EXPECT_EQ(UNDEFINED, parser.ParseType("xyz")); +} diff --git a/cpp/test/s2pdump_test.cpp b/cpp/test/s2pdump_test.cpp new file mode 100644 index 00000000..b845ee71 --- /dev/null +++ b/cpp/test/s2pdump_test.cpp @@ -0,0 +1,71 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022 akuker +// Copyright (C) 2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include + +#include "s2pdump/s2pdump_core.h" +#include "test/test_shared.h" + +using namespace std; +using namespace filesystem; + +TEST(S2pDumpTest, GeneratePropertiesFile) +{ + // Basic test + auto filename = CreateTempFile(0); + S2pDump::inquiry_info_t test_data = { + .vendor = "SCSI2Pi", .product = "TEST PRODUCT", .revision = "REV1", .sector_size = 1000, .capacity = 100}; + test_data.GeneratePropertiesFile(filename); + + string expected_str = +R"({ + "vendor": "SCSI2Pi", + "product": "TEST PRODUCT", + "revision": "REV1", + "block_size": "1000" +} +)"; + EXPECT_EQ(expected_str, ReadTempFileToString(filename)); + DeleteTempFile(filename); + + // Long string test + filename = CreateTempFile(0); + test_data = {.vendor = "01234567", + .product = "0123456789ABCDEF", + .revision = "0123", + .sector_size = UINT32_MAX, + .capacity = UINT64_MAX}; + test_data.GeneratePropertiesFile(filename); + + expected_str = +R"({ + "vendor": "01234567", + "product": "0123456789ABCDEF", + "revision": "0123", + "block_size": "4294967295" +} +)"; + EXPECT_EQ(expected_str, ReadTempFileToString(filename)); + DeleteTempFile(filename); + + // Empty data test + filename = CreateTempFile(0); + test_data = {.vendor = "", .product = "", .revision = "", .sector_size = 0, .capacity = 0}; + test_data.GeneratePropertiesFile(filename); + + expected_str = +R"({ + "vendor": "", + "product": "", + "revision": "" +} +)"; + EXPECT_EQ(expected_str, ReadTempFileToString(filename)); + DeleteTempFile(filename); +} diff --git a/cpp/test/scsi_command_util_test.cpp b/cpp/test/scsi_command_util_test.cpp new file mode 100644 index 00000000..2668ac01 --- /dev/null +++ b/cpp/test/scsi_command_util_test.cpp @@ -0,0 +1,214 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" +#include "shared/shared_exceptions.h" +#include "base/scsi_command_util.h" + +using namespace scsi_command_util; + +TEST(ScsiCommandUtilTest, ModeSelect6) +{ + const int LENGTH = 26; + + vector cdb(6); + vector buf(LENGTH); + + // PF (vendor-specific parameter format) must not fail but be ignored + cdb[1] = 0x00; + ModeSelect(scsi_command::cmd_mode_select6, cdb, buf, LENGTH, 0); + + cdb[0] = 0x15; + // PF (standard parameter format) + cdb[1] = 0x10; + // Request 512 bytes per sector + buf[9] = 0x00; + buf[10] = 0x02; + buf[11] = 0x00; + EXPECT_THAT([&] { ModeSelect(scsi_command::cmd_mode_select6, cdb, buf, LENGTH, 256); }, + Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_parameter_list)))) + << "Requested sector size does not match current sector size"; + + // Page 0 + buf[12] = 0x00; + EXPECT_THAT([&] { ModeSelect(scsi_command::cmd_mode_select6, cdb, buf, LENGTH, 512); }, + Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_parameter_list)))) + << "Unsupported page 0 was not rejected"; + + // Page 3 (Format Device Page) + buf[12] = 0x03; + EXPECT_THAT([&] { ModeSelect(scsi_command::cmd_mode_select6, cdb, buf, LENGTH, 512); }, + Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_parameter_list)))) + << "Requested sector size does not match current sector size"; + + // Match the requested to the current sector size + buf[24] = 0x02; + EXPECT_THAT([&] { ModeSelect(scsi_command::cmd_mode_select6, cdb, buf, LENGTH - 1, 512); }, + Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_parameter_list)))) + << "Not enough command parameters"; + + EXPECT_FALSE(ModeSelect(scsi_command::cmd_mode_select6, cdb, buf, LENGTH, 512).empty()); +} + +TEST(ScsiCommandUtilTest, ModeSelect10) +{ + const int LENGTH = 30; + + vector cdb(10); + vector buf(LENGTH); + + // PF (vendor-specific parameter format) must not fail but be ignored + cdb[1] = 0x00; + ModeSelect(scsi_command::cmd_mode_select10, cdb, buf, LENGTH, 0); + + // PF (standard parameter format) + cdb[1] = 0x10; + // Request 512 bytes per sector + buf[13] = 0x00; + buf[14] = 0x02; + buf[15] = 0x00; + EXPECT_THAT([&] { ModeSelect(scsi_command::cmd_mode_select10, cdb, buf, LENGTH, 256); }, + Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_parameter_list)))) + << "Requested sector size does not match current sector size"; + + // Page 0 + buf[16] = 0x00; + EXPECT_THAT([&] { ModeSelect(scsi_command::cmd_mode_select10, cdb, buf, LENGTH, 512); }, + Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_parameter_list)))) + << "Unsupported page 0 was not rejected"; + + // Page 3 (Format Device Page) + buf[16] = 0x03; + EXPECT_THAT([&] { ModeSelect(scsi_command::cmd_mode_select10, cdb, buf, LENGTH, 512); }, + Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_parameter_list)))) + << "Requested sector size does not match current sector size"; + + // Match the requested to the current sector size + buf[28] = 0x02; + EXPECT_THAT([&] { ModeSelect(scsi_command::cmd_mode_select10, cdb, buf, LENGTH - 1, 512); }, + Throws(AllOf( + Property(&scsi_exception::get_sense_key, sense_key::illegal_request), + Property(&scsi_exception::get_asc, asc::invalid_field_in_parameter_list)))) + << "Not enough command parameters"; + + EXPECT_FALSE(ModeSelect(scsi_command::cmd_mode_select10, cdb, buf, LENGTH, 512).empty()); +} + +TEST(ScsiCommandUtilTest, EnrichFormatPage) +{ + const int SECTOR_SIZE = 512; + + map> pages; + vector format_page(24); + pages[3] = format_page; + + EnrichFormatPage(pages, false, SECTOR_SIZE); + format_page = pages[3]; + EXPECT_EQ(byte{0}, format_page[12]); + EXPECT_EQ(byte{0}, format_page[13]); + + EnrichFormatPage(pages, true, SECTOR_SIZE); + format_page = pages[3]; + EXPECT_EQ(byte{SECTOR_SIZE >> 8}, format_page[12]); + EXPECT_EQ(byte{0}, format_page[13]); +} + +TEST(ScsiCommandUtilTest, AddAppleVendorModePage) +{ + map> pages; + vector vendor_page(30); + pages[48] = vendor_page; + + AddAppleVendorModePage(pages, true); + vendor_page = pages[48]; + EXPECT_EQ(byte{0}, vendor_page[2]); + + AddAppleVendorModePage(pages, false); + vendor_page = pages[48]; + EXPECT_STREQ("APPLE COMPUTER, INC ", (const char *)&vendor_page[2]); +} + +TEST(ScsiCommandUtilTest, GetInt16) +{ + vector b = { 0xfe, 0xdc }; + EXPECT_EQ(0xfedc, GetInt16(b, 0)); + + vector v = { 0x12, 0x34 }; + EXPECT_EQ(0x1234, GetInt16(v, 0)); +} + +TEST(ScsiCommandUtilTest, GetInt24) +{ + vector v = { 0x12, 0x34, 0x56 }; + EXPECT_EQ(0x123456, GetInt24(v, 0)); +} + +TEST(ScsiCommandUtilTest, GetInt32) +{ + vector v = { 0x12, 0x34, 0x56, 0x78 }; + EXPECT_EQ(0x12345678, GetInt32(v, 0)); +} + +TEST(ScsiCommandUtilTest, GetInt64) +{ + vector v = { 0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21 }; + EXPECT_EQ(0x1234567887654321, GetInt64(v, 0)); +} + +TEST(ScsiCommandUtilTest, SetInt16) +{ + vector v(2); + SetInt16(v, 0, 0x1234); + EXPECT_EQ(byte{0x12}, v[0]); + EXPECT_EQ(byte{0x34}, v[1]); +} + +TEST(ScsiCommandUtilTest, SetInt32) +{ + vector buf(4); + SetInt32(buf, 0, 0x12345678); + EXPECT_EQ(0x12, buf[0]); + EXPECT_EQ(0x34, buf[1]); + EXPECT_EQ(0x56, buf[2]); + EXPECT_EQ(0x78, buf[3]); + + vector v(4); + SetInt32(v, 0, 0x12345678); + EXPECT_EQ(byte{0x12}, v[0]); + EXPECT_EQ(byte{0x34}, v[1]); + EXPECT_EQ(byte{0x56}, v[2]); + EXPECT_EQ(byte{0x78}, v[3]); +} + +TEST(ScsiCommandUtilTest, SetInt64) +{ + vector buf(8); + SetInt64(buf, 0, 0x1234567887654321); + EXPECT_EQ(0x12, buf[0]); + EXPECT_EQ(0x34, buf[1]); + EXPECT_EQ(0x56, buf[2]); + EXPECT_EQ(0x78, buf[3]); + EXPECT_EQ(0x87, buf[4]); + EXPECT_EQ(0x65, buf[5]); + EXPECT_EQ(0x43, buf[6]); + EXPECT_EQ(0x21, buf[7]); +} diff --git a/cpp/test/scsi_controller_test.cpp b/cpp/test/scsi_controller_test.cpp new file mode 100644 index 00000000..16133c1b --- /dev/null +++ b/cpp/test/scsi_controller_test.cpp @@ -0,0 +1,282 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" +#include "shared/shared_exceptions.h" + +using namespace scsi_defs; + +TEST(ScsiControllerTest, GetInitiatorId) +{ + const int ID = 2; + + auto bus = make_shared>(); + MockScsiController controller(bus, 0); + + controller.Process(ID); + EXPECT_EQ(ID, controller.GetInitiatorId()); + controller.Process(-1); + EXPECT_EQ(-1, controller.GetInitiatorId()); +} + +TEST(ScsiControllerTest, Process) +{ + auto bus = make_shared>(); + MockScsiController controller(bus, 0); + controller.Init(); + + controller.SetPhase(phase_t::reserved); + ON_CALL(*bus, GetRST).WillByDefault(Return(true)); + EXPECT_CALL(*bus, Acquire); + EXPECT_CALL(*bus, GetRST); + EXPECT_FALSE(controller.Process(0)); + + controller.SetPhase(phase_t::busfree); + ON_CALL(*bus, GetRST).WillByDefault(Return(false)); + EXPECT_CALL(*bus, Acquire); + EXPECT_CALL(*bus, GetRST); + EXPECT_FALSE(controller.Process(0)); + + controller.SetPhase(phase_t::reserved); + EXPECT_CALL(*bus, Acquire).Times(2); + EXPECT_CALL(*bus, GetRST).Times(2); + EXPECT_FALSE(controller.Process(0)); +} + +TEST(ScsiControllerTest, BusFree) +{ + auto bus = make_shared>(); + MockScsiController controller(bus, 0); + + controller.SetPhase(phase_t::busfree); + controller.BusFree(); + EXPECT_EQ(phase_t::busfree, controller.GetPhase()); + + controller.SetStatus(status::check_condition); + controller.SetPhase(phase_t::reserved); + controller.BusFree(); + EXPECT_EQ(phase_t::busfree, controller.GetPhase()); + EXPECT_EQ(status::good, controller.GetStatus()); + + controller.ScheduleShutdown(AbstractController::shutdown_mode::NONE); + controller.SetPhase(phase_t::reserved); + controller.BusFree(); + + controller.ScheduleShutdown(AbstractController::shutdown_mode::STOP_PI); + controller.SetPhase(phase_t::reserved); + controller.BusFree(); + + controller.ScheduleShutdown(AbstractController::shutdown_mode::RESTART_PI); + controller.SetPhase(phase_t::reserved); + controller.BusFree(); + + controller.ScheduleShutdown(AbstractController::shutdown_mode::STOP_S2P); + controller.SetPhase(phase_t::reserved); + controller.BusFree(); +} + +TEST(ScsiControllerTest, Selection) +{ + auto bus = make_shared>(); + auto controller = make_shared(bus, 0); + + controller->SetPhase(phase_t::selection); + ON_CALL(*bus, GetSEL).WillByDefault(Return(true)); + ON_CALL(*bus, GetBSY).WillByDefault(Return(true)); + EXPECT_CALL(*bus, GetATN).Times(0); + controller->Selection(); + EXPECT_EQ(phase_t::selection, controller->GetPhase()); + + ON_CALL(*bus, GetSEL).WillByDefault(Return(true)); + ON_CALL(*bus, GetBSY).WillByDefault(Return(false)); + EXPECT_CALL(*bus, GetATN).Times(0); + EXPECT_CALL(*controller, Status); + controller->Selection(); + EXPECT_EQ(phase_t::selection, controller->GetPhase()); + + ON_CALL(*bus, GetSEL).WillByDefault(Return(false)); + ON_CALL(*bus, GetBSY).WillByDefault(Return(false)); + EXPECT_CALL(*bus, GetATN).Times(0); + controller->Selection(); + EXPECT_EQ(phase_t::selection, controller->GetPhase()); + + ON_CALL(*bus, GetSEL).WillByDefault(Return(false)); + ON_CALL(*bus, GetBSY).WillByDefault(Return(true)); + ON_CALL(*bus, GetATN).WillByDefault(Return(false)); + EXPECT_CALL(*bus, GetATN); + controller->Selection(); + EXPECT_EQ(phase_t::command, controller->GetPhase()); + + controller->SetPhase(phase_t::selection); + ON_CALL(*bus, GetSEL).WillByDefault(Return(false)); + ON_CALL(*bus, GetBSY).WillByDefault(Return(true)); + ON_CALL(*bus, GetATN).WillByDefault(Return(true)); + EXPECT_CALL(*bus, GetATN); + controller->Selection(); + EXPECT_EQ(phase_t::msgout, controller->GetPhase()); + + ON_CALL(*bus, GetDAT).WillByDefault(Return(1)); + EXPECT_CALL(*bus, SetBSY(true)); + controller->Selection(); + EXPECT_EQ(phase_t::selection, controller->GetPhase()); +} + +TEST(ScsiControllerTest, Command) +{ + auto bus = make_shared>(); + MockScsiController controller(bus, 0); + + controller.SetPhase(phase_t::command); + EXPECT_CALL(controller, Status).Times(2); + controller.Command(); + EXPECT_EQ(phase_t::command, controller.GetPhase()); + + controller.SetPhase(phase_t::reserved); + EXPECT_CALL(*bus, SetMSG(false)); + EXPECT_CALL(*bus, SetCD(true)); + EXPECT_CALL(*bus, SetIO(false)); + controller.Command(); + EXPECT_EQ(phase_t::command, controller.GetPhase()); + + controller.SetPhase(phase_t::reserved); + ON_CALL(*bus, CommandHandShake).WillByDefault(Return(6)); + EXPECT_CALL(*bus, SetMSG(false)); + EXPECT_CALL(*bus, SetCD(true)); + EXPECT_CALL(*bus, SetIO(false)); + controller.Command(); + EXPECT_EQ(phase_t::command, controller.GetPhase()); +} + +TEST(ScsiControllerTest, MsgIn) +{ + auto bus = make_shared>(); + MockScsiController controller(bus, 0); + + controller.SetPhase(phase_t::reserved); + EXPECT_CALL(*bus, SetMSG(true)); + EXPECT_CALL(*bus, SetCD(true)); + EXPECT_CALL(*bus, SetIO(true)); + controller.MsgIn(); + EXPECT_EQ(phase_t::msgin, controller.GetPhase()); + EXPECT_FALSE(controller.HasValidLength()); + EXPECT_EQ(0, controller.GetOffset()); +} + +TEST(ScsiControllerTest, MsgOut) +{ + auto bus = make_shared>(); + MockScsiController controller(bus, 0); + + controller.SetPhase(phase_t::reserved); + EXPECT_CALL(*bus, SetMSG(true)); + EXPECT_CALL(*bus, SetCD(true)); + EXPECT_CALL(*bus, SetIO(false)); + controller.MsgOut(); + EXPECT_EQ(phase_t::msgout, controller.GetPhase()); + EXPECT_EQ(1, controller.GetLength()); + EXPECT_EQ(0, controller.GetOffset()); +} + +TEST(ScsiControllerTest, DataIn) +{ + auto bus = make_shared>(); + MockScsiController controller(bus, 0); + + controller.SetPhase(phase_t::reserved); + controller.SetLength(0); + EXPECT_CALL(controller, Status); + controller.DataIn(); + EXPECT_EQ(phase_t::reserved, controller.GetPhase()); + + controller.SetLength(1); + EXPECT_CALL(*bus, SetMSG(false)); + EXPECT_CALL(*bus, SetCD(false)); + EXPECT_CALL(*bus, SetIO(true)); + controller.DataIn(); + EXPECT_EQ(phase_t::datain, controller.GetPhase()); + EXPECT_EQ(0, controller.GetOffset()); +} + +TEST(ScsiControllerTest, DataOut) +{ + auto bus = make_shared>(); + MockScsiController controller(bus, 0); + + controller.SetPhase(phase_t::reserved); + controller.SetLength(0); + EXPECT_CALL(controller, Status); + controller.DataOut(); + EXPECT_EQ(phase_t::reserved, controller.GetPhase()); + + controller.SetLength(1); + EXPECT_CALL(*bus, SetMSG(false)); + EXPECT_CALL(*bus, SetCD(false)); + EXPECT_CALL(*bus, SetIO(false)); + controller.DataOut(); + EXPECT_EQ(phase_t::dataout, controller.GetPhase()); + EXPECT_EQ(0, controller.GetOffset()); +} + +TEST(ScsiControllerTest, Error) +{ + auto bus = make_shared>(); + MockScsiController controller(bus, 0); + + ON_CALL(*bus, GetRST).WillByDefault(Return(true)); + controller.SetPhase(phase_t::reserved); + EXPECT_CALL(*bus, Acquire); + EXPECT_CALL(*bus, GetRST()); + EXPECT_CALL(controller, Reset).Times(0); + controller.Error(sense_key::aborted_command, asc::no_additional_sense_information, status::reservation_conflict); + EXPECT_EQ(status::good, controller.GetStatus()); + EXPECT_EQ(phase_t::busfree, controller.GetPhase()); + + ON_CALL(*bus, GetRST).WillByDefault(Return(false)); + controller.SetPhase(phase_t::status); + EXPECT_CALL(*bus, Acquire); + EXPECT_CALL(*bus, GetRST()); + EXPECT_CALL(controller, Reset).Times(0); + controller.Error(sense_key::aborted_command, asc::no_additional_sense_information, status::reservation_conflict); + EXPECT_EQ(phase_t::busfree, controller.GetPhase()); + + controller.SetPhase(phase_t::msgin); + EXPECT_CALL(*bus, Acquire); + EXPECT_CALL(*bus, GetRST()); + EXPECT_CALL(controller, Reset).Times(0); + controller.Error(sense_key::aborted_command, asc::no_additional_sense_information, status::reservation_conflict); + EXPECT_EQ(phase_t::busfree, controller.GetPhase()); + + controller.SetPhase(phase_t::reserved); + EXPECT_CALL(*bus, Acquire); + EXPECT_CALL(*bus, GetRST()); + EXPECT_CALL(controller, Reset).Times(0); + EXPECT_CALL(controller, Status); + controller.Error(sense_key::aborted_command, asc::no_additional_sense_information, status::reservation_conflict); + EXPECT_EQ(status::reservation_conflict, controller.GetStatus()); + EXPECT_EQ(phase_t::reserved, controller.GetPhase()); +} + +TEST(ScsiControllerTest, RequestSense) +{ + auto bus = make_shared>(); + auto controller = make_shared(bus, 0); + auto device = make_shared(0); + EXPECT_TRUE(device->Init({})); + + controller->AddDevice(device); + + // ALLOCATION LENGTH + controller->SetCmdByte(4, 255); + // Non-existing LUN + controller->SetCmdByte(1, 0x20); + + device->SetReady(true); + EXPECT_CALL(*controller, Status); + device->Dispatch(scsi_command::cmd_request_sense); + EXPECT_EQ(status::good, controller->GetStatus()) << "Wrong CHECK CONDITION for non-existing LUN"; +} diff --git a/cpp/test/scsi_hd_test.cpp b/cpp/test/scsi_hd_test.cpp new file mode 100644 index 00000000..46a9157d --- /dev/null +++ b/cpp/test/scsi_hd_test.cpp @@ -0,0 +1,126 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" +#include "shared/shared_exceptions.h" +#include "devices/scsi_hd.h" + +void ScsiHdTest_SetUpModePages(map>& pages) +{ + EXPECT_EQ(5, pages.size()) << "Unexpected number of mode pages"; + EXPECT_EQ(12, pages[1].size()); + EXPECT_EQ(24, pages[3].size()); + EXPECT_EQ(24, pages[4].size()); + EXPECT_EQ(12, pages[8].size()); + EXPECT_EQ(30, pages[48].size()); +} + +TEST(ScsiHdTest, Inquiry) +{ + TestInquiry::Inquiry(SCHD, device_type::direct_access, scsi_level::scsi_2, "SCSI2Pi ", 0x1f, false); + + TestInquiry::Inquiry(SCHD, device_type::direct_access, scsi_level::scsi_1_ccs, "SCSI2Pi ", 0x1f, false, "file.hd1"); +} + +TEST(ScsiHdTest, SupportsSaveParameters) +{ + MockScsiHd hd(0, false); + + EXPECT_TRUE(hd.SupportsSaveParameters()); +} + +TEST(ScsiHdTest, FinalizeSetup) +{ + MockScsiHd hd(0, false); + + hd.SetSectorSizeInBytes(1024); + EXPECT_THROW(hd.FinalizeSetup(0), io_exception) << "Device has 0 blocks"; +} + +TEST(ScsiHdTest, GetProductData) +{ + MockScsiHd hd_kb(0, false); + MockScsiHd hd_mb(0, false); + MockScsiHd hd_gb(0, false); + + const path filename = CreateTempFile(1); + hd_kb.SetFilename(string(filename)); + hd_kb.SetSectorSizeInBytes(1024); + hd_kb.SetBlockCount(1); + hd_kb.FinalizeSetup(0); + string s = hd_kb.GetProduct(); + EXPECT_NE(string::npos, s.find("1 KiB")); + + hd_mb.SetFilename(string(filename)); + hd_mb.SetSectorSizeInBytes(1024); + hd_mb.SetBlockCount(1'048'576 / 1024); + hd_mb.FinalizeSetup(0); + s = hd_mb.GetProduct(); + EXPECT_NE(string::npos, s.find("1 MiB")); + + hd_gb.SetFilename(string(filename)); + hd_gb.SetSectorSizeInBytes(1024); + hd_gb.SetBlockCount(10'737'418'240 / 1024); + hd_gb.FinalizeSetup(0); + s = hd_gb.GetProduct(); + EXPECT_NE(string::npos, s.find("10 GiB")); + remove(filename); +} + +TEST(ScsiHdTest, GetSectorSizes) +{ + MockScsiHd hd(0, false); + + const auto& sector_sizes = hd.GetSupportedSectorSizes(); + EXPECT_EQ(4, sector_sizes.size()); + + EXPECT_TRUE(sector_sizes.contains(512)); + EXPECT_TRUE(sector_sizes.contains(1024)); + EXPECT_TRUE(sector_sizes.contains(2048)); + EXPECT_TRUE(sector_sizes.contains(4096)); +} + +TEST(ScsiHdTest, SetUpModePages) +{ + map> pages; + MockScsiHd hd(0, false); + + // Non changeable + hd.SetUpModePages(pages, 0x3f, false); + ScsiHdTest_SetUpModePages(pages); + + // Changeable + pages.clear(); + hd.SetUpModePages(pages, 0x3f, true); + ScsiHdTest_SetUpModePages(pages); +} + +TEST(ScsiHdTest, ModeSelect) +{ + MockScsiHd hd({ 512 }); + vector cmd(10); + vector buf(255); + + hd.SetSectorSizeInBytes(512); + + // PF + cmd[1] = 0x10; + // Page 3 (Device Format Page) + buf[4] = 0x03; + // 512 bytes per sector + buf[16] = 0x02; + EXPECT_NO_THROW(hd.ModeSelect(scsi_command::cmd_mode_select6, cmd, buf, 255)) << "MODE SELECT(6) is supported"; + buf[4] = 0; + buf[16] = 0; + + // Page 3 (Device Format Page) + buf[8] = 0x03; + // 512 bytes per sector + buf[20] = 0x02; + EXPECT_NO_THROW(hd.ModeSelect(scsi_command::cmd_mode_select10, cmd, buf, 255)) << "MODE SELECT(10) is supported"; +} diff --git a/cpp/test/shared_exceptions_test.cpp b/cpp/test/shared_exceptions_test.cpp new file mode 100644 index 00000000..25c72725 --- /dev/null +++ b/cpp/test/shared_exceptions_test.cpp @@ -0,0 +1,52 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include + +#include "shared/shared_exceptions.h" + +using namespace scsi_defs; + +TEST(SharedExceptionsTest, IoException) +{ + try { + throw io_exception("msg"); + } + catch(const io_exception& e) { + EXPECT_STREQ("msg", e.what()); + } +} + +TEST(SharedExceptionsTest, FileNotFoundException) +{ + try { + throw file_not_found_exception("msg"); + } + catch(const file_not_found_exception& e) { + EXPECT_STREQ("msg", e.what()); + } +} + +TEST(SharedExceptionsTest, ScsiErrorException) +{ + try { + throw scsi_exception(sense_key::unit_attention); + } + catch(const scsi_exception& e) { + EXPECT_EQ(sense_key::unit_attention, e.get_sense_key()); + EXPECT_EQ(asc::no_additional_sense_information, e.get_asc()); + } + + try { + throw scsi_exception(sense_key::illegal_request, asc::lba_out_of_range); + } + catch(const scsi_exception& e) { + EXPECT_EQ(sense_key::illegal_request, e.get_sense_key()); + EXPECT_EQ(asc::lba_out_of_range, e.get_asc()); + } +} diff --git a/cpp/test/storage_device_test.cpp b/cpp/test/storage_device_test.cpp new file mode 100644 index 00000000..6fcf2624 --- /dev/null +++ b/cpp/test/storage_device_test.cpp @@ -0,0 +1,162 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" +#include "shared/s2p_util.h" +#include "shared/shared_exceptions.h" +#include "devices/storage_device.h" +#include + +using namespace filesystem; + +TEST(StorageDeviceTest, SetGetFilename) +{ + MockStorageDevice device; + + device.SetFilename("filename"); + EXPECT_EQ("filename", device.GetFilename()); +} + +TEST(StorageDeviceTest, ValidateFile) +{ + MockStorageDevice device; + + device.SetBlockCount(0); + device.SetFilename("/non_existing_file"); + EXPECT_THROW(device.ValidateFile(), io_exception); + + device.SetBlockCount(1); + EXPECT_THROW(device.ValidateFile(), io_exception); + + const path filename = CreateTempFile(1); + device.SetFilename(string(filename)); + device.SetReadOnly(false); + device.SetProtectable(true); + device.ValidateFile(); + EXPECT_FALSE(device.IsReadOnly()); + EXPECT_TRUE(device.IsProtectable()); + EXPECT_FALSE(device.IsStopped()); + EXPECT_FALSE(device.IsRemoved()); + EXPECT_FALSE(device.IsLocked()); + + permissions(filename, perms::owner_read); + device.SetReadOnly(false); + device.SetProtectable(true); + device.ValidateFile(); + EXPECT_TRUE(device.IsReadOnly()); + EXPECT_FALSE(device.IsProtectable()); + EXPECT_FALSE(device.IsProtected()); + EXPECT_FALSE(device.IsStopped()); + EXPECT_FALSE(device.IsRemoved()); + EXPECT_FALSE(device.IsLocked()); + + remove(filename); +} + +TEST(StorageDeviceTest, MediumChanged) +{ + MockStorageDevice device; + + EXPECT_FALSE(device.IsMediumChanged()); + + device.SetMediumChanged(true); + EXPECT_TRUE(device.IsMediumChanged()); + + device.SetMediumChanged(false); + EXPECT_FALSE(device.IsMediumChanged()); +} + +TEST(StorageDeviceTest, GetIdsForReservedFile) +{ + const int ID = 1; + const int LUN = 0; + auto bus = make_shared(); + ControllerFactory controller_factory; + MockAbstractController controller(bus, ID); + auto device = make_shared(LUN, false); + device->SetFilename("filename"); + StorageDevice::UnreserveAll(); + + EXPECT_TRUE(controller_factory.AttachToController(*bus, ID, device)); + + const auto [id1, lun1] = StorageDevice::GetIdsForReservedFile("filename"); + EXPECT_EQ(-1, id1); + EXPECT_EQ(-1, lun1); + + device->ReserveFile(); + const auto [id2, lun2] = StorageDevice::GetIdsForReservedFile("filename"); + EXPECT_EQ(ID, id2); + EXPECT_EQ(LUN, lun2); + + device->UnreserveFile(); + const auto [id3, lun3] = StorageDevice::GetIdsForReservedFile("filename"); + EXPECT_EQ(-1, id3); + EXPECT_EQ(-1, lun3); +} + +TEST(StorageDeviceTest, UnreserveAll) +{ + const int ID = 1; + const int LUN = 0; + auto bus = make_shared(); + ControllerFactory controller_factory; + MockAbstractController controller(bus, ID); + auto device = make_shared(LUN, false); + device->SetFilename("filename"); + + EXPECT_TRUE(controller_factory.AttachToController(*bus, ID, device)); + + device->ReserveFile(); + StorageDevice::UnreserveAll(); + const auto [id, lun] = StorageDevice::GetIdsForReservedFile("filename"); + EXPECT_EQ(-1, id); + EXPECT_EQ(-1, lun); +} + +TEST(StorageDeviceTest, GetSetReservedFiles) +{ + const int ID = 1; + const int LUN = 0; + auto bus = make_shared(); + ControllerFactory controller_factory; + MockAbstractController controller(bus, ID); + auto device = make_shared(LUN, false); + device->SetFilename("filename"); + + EXPECT_TRUE(controller_factory.AttachToController(*bus, ID, device)); + + device->ReserveFile(); + + const auto& reserved_files = StorageDevice::GetReservedFiles(); + EXPECT_EQ(1, reserved_files.size()); + EXPECT_TRUE(reserved_files.contains("filename")); + + StorageDevice::SetReservedFiles(reserved_files); + EXPECT_EQ(1, reserved_files.size()); + EXPECT_TRUE(reserved_files.contains("filename")); +} + +TEST(StorageDeviceTest, FileExists) +{ + EXPECT_FALSE(StorageDevice::FileExists("/non_existing_file")); + EXPECT_TRUE(StorageDevice::FileExists("/dev/null")); +} + +TEST(StorageDeviceTest, GetFileSize) +{ + MockStorageDevice device; + + const path filename = CreateTempFile(512); + device.SetFilename(filename.c_str()); + const off_t size = device.GetFileSize(); + remove(filename); + EXPECT_EQ(512, size); + + device.SetFilename("/non_existing_file"); + EXPECT_THROW(device.GetFileSize(), io_exception); +} diff --git a/cpp/test/test_setup.cpp b/cpp/test/test_setup.cpp new file mode 100644 index 00000000..53d34287 --- /dev/null +++ b/cpp/test/test_setup.cpp @@ -0,0 +1,35 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include + +#include + +int main(int argc, char *[]) +{ + const bool disable_logging = argc <= 1; + + // If any argument is provided the log level is set to trace + spdlog::set_level(disable_logging ? spdlog::level::off : spdlog::level::trace); + + int fd = -1; + if (disable_logging) { + fd = open("/dev/null", O_WRONLY); + dup2(fd, STDERR_FILENO); + } + + testing::InitGoogleTest(); + + const int result = RUN_ALL_TESTS(); + + if (fd != -1) { + close(fd); + } + + return result; +} diff --git a/cpp/test/test_shared.cpp b/cpp/test/test_shared.cpp new file mode 100644 index 00000000..9dd61aaf --- /dev/null +++ b/cpp/test/test_shared.cpp @@ -0,0 +1,127 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#include "mocks.h" +#include "shared/shared_exceptions.h" +#include "shared/s2p_version.h" +#include +#include +#include +#include + +using namespace std; +using namespace filesystem; + +// Inlude the process id in the temp file path so that multiple instances of the test procedures +// could run on the same host. +const path test_data_temp_path(temp_directory_path() / + path(fmt::format("scsi2pi-test-{}", + getpid()))); // NOSONAR Publicly writable directory is fine here + +pair, shared_ptr> CreateDevice(PbDeviceType type, const string& extension) +{ + DeviceFactory device_factory; + + auto controller = make_shared>(0); + auto device = device_factory.CreateDevice(type, 0, extension); + device->Init({}); + + EXPECT_TRUE(controller->AddDevice(device)); + + return { controller, device }; +} + +void TestInquiry::Inquiry(PbDeviceType type, device_type t, scsi_level l, const string& ident, int additional_length, + bool removable, const string& extension) +{ + auto [controller, device] = CreateDevice(type, extension); + + // ALLOCATION LENGTH + controller->SetCmdByte(4, 255); + EXPECT_CALL(*controller, DataIn()); + device->Dispatch(scsi_command::cmd_inquiry); + const vector& buffer = controller->GetBuffer(); + EXPECT_EQ(t, static_cast(buffer[0])); + EXPECT_EQ(removable ? 0x80 : 0x00, buffer[1]); + EXPECT_EQ(l, static_cast(buffer[2])); + EXPECT_EQ(l > scsi_level::scsi_2 ? scsi_level::scsi_2 : l, static_cast(buffer[3])); + EXPECT_EQ(additional_length, buffer[4]); + string product_data; + if (ident.size() == 24) { + ostringstream s; + s << ident << setw(2) << setfill('0') << s2p_major_version << setw(2) << s2p_minor_version; + product_data = s.str(); + } else { + product_data = ident; + } + EXPECT_TRUE(!memcmp(product_data.c_str(), &buffer[8], 28)); +} + +pair OpenTempFile() +{ + const string filename = + string(test_data_temp_path) + "/scsi2pi_test-XXXXXX"; // NOSONAR Publicly writable directory is fine here + vector f(filename.begin(), filename.end()); + f.push_back(0); + + create_directories(path(filename).parent_path()); + + const int fd = mkstemp(f.data()); + EXPECT_NE(-1, fd) << "Couldn't create temporary file '" << f.data() << "'"; + + return make_pair(fd, path(f.data())); +} + +path CreateTempFile(int size) +{ + const auto data = vector(size); + return CreateTempFileWithData(data); +} + +path CreateTempFileWithData(const span data) +{ + const auto [fd, filename] = OpenTempFile(); + + const size_t count = write(fd, data.data(), data.size()); + EXPECT_EQ(count, data.size()) << "Couldn't create temporary file '" << string(filename) << "'"; + close(fd); + + return path(filename); +} + +void DeleteTempFile(const string& filename) +{ + path temp_file = test_data_temp_path; + temp_file += path(filename); + remove(temp_file); +} + +string ReadTempFileToString(const string& filename) +{ + const path temp_file = test_data_temp_path / path(filename); + ifstream in(temp_file); + stringstream buffer; + buffer << in.rdbuf(); + + return buffer.str(); +} + +int GetInt16(const vector& buf, int offset) +{ + assert(buf.size() > static_cast(offset) + 1); + + return (to_integer(buf[offset]) << 8) | to_integer(buf[offset + 1]); +} + +uint32_t GetInt32(const vector& buf, int offset) +{ + assert(buf.size() > static_cast(offset) + 3); + + return (to_integer(buf[offset]) << 24) | (to_integer(buf[offset + 1]) << 16) | + (to_integer(buf[offset + 2]) << 8) | to_integer(buf[offset + 3]); +} diff --git a/cpp/test/test_shared.h b/cpp/test/test_shared.h new file mode 100644 index 00000000..0dd99346 --- /dev/null +++ b/cpp/test/test_shared.h @@ -0,0 +1,45 @@ +//--------------------------------------------------------------------------- +// +// SCSI target emulator and SCSI initiator tools for the Raspberry Pi +// +// Copyright (C) 2022-2023 Uwe Seimet +// +//--------------------------------------------------------------------------- + +#pragma once + +#include "generated/command_interface.pb.h" +#include "shared/scsi.h" +#include +#include +#include +#include + +using namespace std; +using namespace filesystem; +using namespace command_interface; + +class PrimaryDevice; +class MockAbstractController; + +extern const path test_data_temp_path; + +pair, shared_ptr> CreateDevice(PbDeviceType, const string& = ""); + +pair OpenTempFile(); +path CreateTempFile(int); +path CreateTempFileWithData(span); + +void DeleteTempFile(const string&); + +string ReadTempFileToString(const string& filename); + +int GetInt16(const vector&, int); +uint32_t GetInt32(const vector&, int); + +// This class is needed in order to be declared as friend, required to have access to AbstractController::SetCmdByte +class TestInquiry { +public: + static void Inquiry(PbDeviceType, scsi_defs::device_type, scsi_defs::scsi_level, const string&, int, bool, + const string& = ""); +}; diff --git a/ide_setup/README b/ide_setup/README new file mode 100644 index 00000000..3b0d2b6a --- /dev/null +++ b/ide_setup/README @@ -0,0 +1,5 @@ +The Eclipse code formatter configuration shall be used together with +Eclipse CDT in order to unify the formatting of the C++ code. Ensure to keep +your formatting rules up to date. + +This formatter can also be imported into Intellij IDEA. diff --git a/ide_setup/eclipse_code_formatter.xml b/ide_setup/eclipse_code_formatter.xml new file mode 100644 index 00000000..242f35bc --- /dev/null +++ b/ide_setup/eclipse_code_formatter.xml @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +