diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..431496f --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,36 @@ +name: .NET + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Fetch history + run: git fetch --prune --unshallow + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 9.0.x + - name: Restore dependencies + run: | + dotnet restore src/S7CommPlusDriver + dotnet restore src/DriverTest + dotnet restore src/S7CommPlusGUIBrowser + + - name: Build + run: | + dotnet build src/S7CommPlusDriver + dotnet build src/DriverTest + dotnet build src/S7CommPlusGUIBrowser + - uses: actions/upload-artifact@v4 + with: + path: src/S7CommPlusDriver/bin/Debug/*.nupkg \ No newline at end of file diff --git a/.github/workflows/dotnetpull.yml b/.github/workflows/dotnetpull.yml new file mode 100644 index 0000000..e8de32f --- /dev/null +++ b/.github/workflows/dotnetpull.yml @@ -0,0 +1,30 @@ +name: .NET Pull Request + +on: + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 8.0.x + 9.0.x + - name: Restore dependencies + run: | + dotnet restore src/S7CommPlusDriver + dotnet restore src/DriverTest + dotnet restore src/S7CommPlusGUIBrowser + - name: Build + run: | + dotnet build src/S7CommPlusDriver + dotnet build src/DriverTest + dotnet build src/S7CommPlusGUIBrowser diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..94954b3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,45 @@ +name: .NET + +on: + release: + types: [published] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Fetch history + run: git fetch --prune --unshallow + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 8.0.x + 9.0.x + - name: Restore dependencies + run: | + dotnet restore src/S7CommPlusDriver + dotnet restore src/DriverTest + dotnet restore src/S7CommPlusGUIBrowser + + - name: Update project version + uses: roryprimrose/set-vs-sdk-project-version@v1 + with: + version: ${{ github.event.release.tag_name }} + assemblyVersion: ${{ github.event.release.tag_name }} + fileVersion: ${{ github.event.release.tag_name }} + informationalVersion: ${{ github.event.release.tag_name }}-${{ github.sha }} + + - name: Build + run: | + dotnet build --configuration Release src/S7CommPlusDriver + dotnet build --configuration Release src/DriverTest + dotnet build --configuration Release src/S7CommPlusGUIBrowser + - name: Publish + run: | + dotnet nuget push --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json src/S7CommPlusDriver/bin/Release/*.nupkg + \ No newline at end of file diff --git a/README.md b/README.md index 86da2b5..0625bd8 100644 --- a/README.md +++ b/README.md @@ -1,185 +1,428 @@ -# S7commPlusDriver +# S7CommPlusDriver -Kommunikationstreiber für den Datenaustausch mit S7-1200/1500 Steuerungen. +[![NuGet](https://img.shields.io/nuget/v/DotNetProjects.S7CommPlusDriver.svg)](https://www.nuget.org/packages/DotNetProjects.S7CommPlusDriver) +[![NuGet downloads](https://img.shields.io/nuget/dt/DotNetProjects.S7CommPlusDriver.svg)](https://www.nuget.org/packages/DotNetProjects.S7CommPlusDriver) +[![Build](https://github.com/dotnetprojects/S7CommPlusDriver/actions/workflows/dotnet.yml/badge.svg)](https://github.com/dotnetprojects/S7CommPlusDriver/actions/workflows/dotnet.yml) +[![Release](https://github.com/dotnetprojects/S7CommPlusDriver/actions/workflows/release.yml/badge.svg)](https://github.com/dotnetprojects/S7CommPlusDriver/actions/workflows/release.yml) +[![License](https://img.shields.io/github/license/dotnetprojects/S7CommPlusDriver.svg)](LICENSE) -## Entwicklungsstand +Production-oriented .NET communication library for Siemens S7-1200/1500 PLCs using S7CommPlus over TLS, with optional legacy challenge authentication for pre-V17/pre-TLS CPUs on `net8.0` and later. -Dies ist aktuell ein Entwicklungsstand und nicht für den Produktiveinsatz vorgesehen. - -Ziel ist es, einen Kommunikationstreiber zu entwickeln, welcher den Zugriff auf den Variablenhaushalt -von S7 1200/1500 Steuerungen über den symbolischen Zugriff auf sogenannte "optimierte" Bereiche erlaubt. - -Diese Implementierung ist vollständig in C# verfasst. Für die TLS-Verschlüsselung wird die OpenSSL-Bibliothek verwendet. - -## Systemvorraussetzungen - -### CPU -Der Kommunikationstreiber unterstützt **ausschließlich** CPUs mit einer Firmware welche die sichere Kommunikation -über das TLS-Protokoll erlaubt. Das wären nach aktuellem Wissensstand -- S7 1200 mit einer Firmware >= V4.3 (TLS 1.3 ab V4.5) -- S7 1500 mit einer Firmware >= V2.9 - -Wichtig ist dabei, dass nicht nur nur eine CPU mit entsprechender Firmware vorhanden ist, sondern auch in der Entwicklungsumgebung -mit der ensprechenden Version projektiert wurde. Das ist nur mit einer TIA-Portal Version >= V17 möglich. - -### OpenSSL -Für die TLS-Kommunikation wird OpenSSL verwendet. Ist OpenSSL in der entsprechenden Version installiert, dann sollte ein entsprechender -Systempfad zum Installationsverzeichnis eingetragen sein. Die notwendigen dlls sind aber auch im Projekt abgelegt, und werden im Buildprozess -in der notwendigen Version (x86 oder x64) in das Ausgabeverzeichnis kopiert. -Die notwendigen dlls mit Dateinamen je nach verwendetem Betriebssystem: - -Für 32 Bit (x86): -- libcrypto-3.dll -- libssl-3.dll - -Für 64 Bit (x64): -- libcrypto-3-x64.dll -- libssl-3-x64.dll - -## Getestete Kommunikation -Mit folgenden Geräten wurde bisher erfolgreich getestet: -- S7 1211 mit Firmware V4.5 -- TIA Plcsim V17 (mit Nettoplcsim) -- TIA Plcsim V18 (mit Nettoplcsim) - -## Analyse mit Wireshark -Aufgrund der Verschlüsselung können die übertragenen Daten ohne weitere Informationen mit Wireshark nicht mehr eingesehen werden. -Zur Treiberentwicklung ist im Projekt eine Funktion integriert, welche die ausgehandelten Secrets in eine Textdatei -(key_YYYYMMDD_hhmmss.log) ausgibt. Mit diesen Informationen ist es Wireshark möglich die Kommunikation zu entschlüsseln und darzustellen. -Wichtig ist dabei, dass die Aufzeichnung den TLS-Verbindungsaufbau enthalten muss! - -Um Wireshark diese Information verfügbar zu machen, existieren zwei Möglichkeiten: -1. Die Log-Datei in ein Verzeichnis abzulegen und Wireshark dieses bekannt zu machen. Dazu in Wireshark *Menü* → *Einstellungen* aufrufen. -Unter *Protocols* den Punkt *TLS* anwählen, und im Feld *(Pre)-Master-Secret log filename* die entsprechende Datei auswählen -2. Die Secrets direkt in die Wireshark-Aufzeichnung integrieren - -Zur Weitergabe an andere Personen zur Analyse, ist Punkt 2 zu bevorzugen, da alles notwendige in einer Aufzeichnung vorhanden ist. -Die Integration geschieht über das Programm "editcap.exe" im Wireshark Installationsverzeichnis. Dazu muss eine Aufzeichnung in -Wireshark mit der Endung *.pcapng* gespeichert werden. - -Über die Eingabeaufforderung werden mit folgender Anweisung in die Aufzeichnung "test-capture.pcapng" die Secrets aus "key.log" -integriert, und in der Datei "test-capture-with-keys.pcapng" gespeichert. Wird letztere Datei dann in Wireshark geöffnet, kann die -Kommunikation dort entschlüsselt, dekodiert und dem Protokoll entsprechend dargestellt werden. -Die key.log kann bei Bedarf anschließend gelöscht werden. -``` -"C:\Program Files\Wireshark\editcap.exe" --inject-secrets tls,key.log test-capture.pcapng test-capture-with-keys.pcapng -``` - -Zur Vereinfachung habe ich ein kleines Hilfsprogramm mit einer grafischen Oberfläche geschrieben, auf das die Dateien per Drag&Drop -gezogen werden können, und das auf Tastendruck editcap aufruft. Das Programm ist hier verfügbar: - -https://github.com/thomas-v2/PcapKeyInjector - -Damit Wireshark das S7comm-Plus Protokoll dekodieren kann, ist die entsprechende dll in das Wireshark Installationsverzeichnis abzulegen. -Näheres dazu, und Download der dll bei Sourceforge unter: - -https://sourceforge.net/projects/s7commwireshark/ - -## PlcTag-Klasse: Umsetzung der SPS Datentypen in PlcTags - -Bei einigen Datentypen ist es notwendig, zur Verarbeitung der Antwort der SPS den Typ vorab zu kennen, um ihn in ein -sinnvollen Datentyp in .Net zu konvertieren. Dazu wird die PlcTag Klasse bereitgestellt. - -In der Tabelle sind alle in der SPS zur Zeit (TIA V18) möglichen Datentypen aufgeführt, mit dem Datentyp in dem sie -auf dem Netzwerk im S7comm-Plus-Protokoll übertragen werden, sowie welchen .Net Datentyp in den PlcTag-Klassen daraus -resultiert. - -| Supported | PLC Datentyp | PLC Kategorie | PLC Info | Netzwerk Datentyp | .Net Datentyp PlcTag | Sonstiges | -| :-------: | --------------------------| ----------------- | ----------------- | ----------------------------- | ----------------------------- | ------------------------------------------------- | -| ✓ | AOM_IDENT | Hardwaredatentypen| | ValueDWord | PlcTagDWord -> uint | | -| ✓ | Any | Zeiger | Parameter | ValueUSIntArray[10] | byte[10] | | -| ✓ | Array[n..m] | | | | | Zugriff auf Einzelelemente direkt möglich | -| ✓ | Block_FB | Parametertypen | Parameter | ValueUInt | PlcTagUInt -> ushort | | -| ✓ | Block_FC | Parametertypen | Parameter | ValueUInt | PlcTagUInt -> ushort | | -| ✓ | Bool | Binärzahlen | | ValueBool | bool | | -| ✓ | Byte | Bitfolgen | | ValueByte | byte | | -| ✓ | CONN_ANY | Hardwaredatentypen| | ValueWord | PlcTagWord -> ushort | | -| ✓ | CONN_OUC | Hardwaredatentypen| | ValueWord | PlcTagWord -> ushort | | -| ✓ | CONN_PRG | Hardwaredatentypen| | ValueWord | PlcTagWord -> ushort | | -| ✓ | CONN_R_ID | Hardwaredatentypen| | ValueDWord | PlcTagDWord -> uint | | -| ✓ | CREF | Systemdatentypen | | ValueStruct / packed | | Zugriff auf Einzelelemente direkt möglich | -| ✓ | Char | Zeichenfolgen | | ValueUSInt | char | Encoding Voreinstellung ISO-8859-1 für non-ASCII | -| ✓ | Counter | Parametertypen | Parameter | ValueUInt | PlcTagUInt -> ushort | | -| ✓ | Date | Datum und Uhrzeit | | ValueUInt | DateTime | TODO: Nur Datum gültig! | -| ✓ | Date_And_Time | Datum und Uhrzeit | | ValueUSIntArray[8] | DateTime | | -| ✓ | DB_ANY | Hardwaredatentypen| | ValueUInt | PlcTagUInt -> ushort | | -| ✓ | DB_DYN | Hardwaredatentypen| | ValueUInt | PlcTagUInt -> ushort | | -| ✓ | DB_WWW | Hardwaredatentypen| | ValueUInt | PlcTagUInt -> ushort | | -| ✓ | DInt | Ganzzahlen | | ValueDInt | int | | -| ✓ | DTL | Datum und Uhrzeit | | ValueStruct / packed | DateTime + uint (for ns) | Nanosekunden extern, da kein .Net Typ mit ns. Experimental! | -| ✓ | DWord | Bitfolgen | | ValueDWord | uint | | -| ✓ | EVENT_ANY | Hardwaredatentypen| | ValueDWord | PlcTagDWord -> uint | | -| ✓ | EVENT_ATT | Hardwaredatentypen| | ValueDWord | PlcTagDWord -> uint | | -| ✓ | EVENT_HWINT | Hardwaredatentypen| | ValueDWord | PlcTagDWord -> uint | | -| ✓ | ErrorStruct | | | ValueStruct / packed | | Zugriff auf Einzelelemente direkt möglich | -| ✓ | HW_ANY | Hardwaredatentypen| | ValueWord | | | -| ✓ | HW_DEVICE | Hardwaredatentypen| | ValueWord | PlcTagWord -> ushort | | -| ✓ | HW_DPMASTER | Hardwaredatentypen| | ValueWord | PlcTagWord -> ushort | | -| ✓ | HW_DPSLAVE | Hardwaredatentypen| | ValueWord | PlcTagWord -> ushort | | -| ✓ | HW_HSC | Hardwaredatentypen| | ValueWord | PlcTagWord -> ushort | | -| ✓ | HW_IEPORT | Hardwaredatentypen| | ValueWord | PlcTagWord -> ushort | | -| ✓ | HW_INTERFACE | Hardwaredatentypen| | ValueWord | PlcTagWord -> ushort | | -| ✓ | HW_IO | Hardwaredatentypen| | ValueWord | PlcTagWord -> ushort | | -| ✓ | HW_IOSYSTEM | Hardwaredatentypen| | ValueWord | PlcTagWord -> ushort | | -| ✓ | HW_MODULE | Hardwaredatentypen| | ValueWord | PlcTagWord -> ushort | | -| ✓ | HW_PTO | Hardwaredatentypen| | ValueWord | PlcTagWord -> ushort | | -| ✓ | HW_PWM | Hardwaredatentypen| | ValueWord | PlcTagWord -> ushort | | -| ✓ | HW_SUBMODULE | Hardwaredatentypen| | ValueWord | PlcTagWord -> ushort | | -| ✓ | IEC_COUNTER | Systemdatentypen | | ValueStruct / packed | | 33554462, Zugriff auf Einzelelemente direkt möglich | -| ✓ | IEC_DCOUNTER | Systemdatentypen | | ValueStruct / packed | | Zugriff auf Einzelelemente direkt möglich | -| ✓ | IEC_LCOUNTER | Systemdatentypen | | ValueStruct / packed | | Zugriff auf Einzelelemente direkt möglich | -| ✓ | IEC_LTIMER | Systemdatentypen | | ValueStruct / packed | | Zugriff auf Einzelelemente direkt möglich | -| ✓ | IEC_SCOUNTER | Systemdatentypen | | ValueStruct / packed | | Zugriff auf Einzelelemente direkt möglich | -| ✓ | IEC_TIMER | Systemdatentypen | | ValueStruct / packed | | 33554463, Zugriff auf Einzelelemente direkt möglich | -| ✓ | IEC_UCOUNTER | Systemdatentypen | | ValueStruct / packed | | Zugriff auf Einzelelemente direkt möglich | -| ✓ | IEC_UDCOUNTER | Systemdatentypen | | ValueStruct / packed | | Zugriff auf Einzelelemente direkt möglich | -| ✓ | IEC_ULCOUNTER | Systemdatentypen | | ValueStruct / packed | | Zugriff auf Einzelelemente direkt möglich | -| ✓ | IEC_USCOUNTER | Systemdatentypen | | ValueStruct / packed | | Zugriff auf Einzelelemente direkt möglich | -| ✓ | Int | Ganzzahlen | | ValueInt | short | | -| ✓ | LDT | Datum und Uhrzeit | | ValueTimestamp | ulong | | -| ✓ | LInt | Ganzzahlen | | ValueLInt | long | | -| ✓ | LReal | Gleitpunktzahlen | | ValueLReal | double | | -| ✓ | LTime | Zeiten | | ValueTimespan | long | Anzahl ns | -| ✓ | LTime_Of_Day (LTOD) | Datum und Uhrzeit | | ValueULInt | ulong | Anzahl ns seit 00:00:00 Uhr | -| ✓ | LWord | Bitfolgen | | ValueLWord | ulong | | -| ✓ | NREF | Systemdatentypen | | ValueStruct / packed | | Zugriff auf Einzelelemente direkt möglich | -| ✓ | OB_ANY | Hardwaredatentypen| | ValueInt | PlcTagInt -> short | | -| ✓ | OB_ATT | Hardwaredatentypen| | ValueInt | PlcTagInt -> short | | -| ✓ | OB_CYCLIC | Hardwaredatentypen| | ValueInt | PlcTagInt -> short | | -| ✓ | OB_DELAY | Hardwaredatentypen| | ValueInt | PlcTagInt -> short | | -| ✓ | OB_DIAG | Hardwaredatentypen| | ValueInt | PlcTagInt -> short | | -| ✓ | OB_HWINT | Hardwaredatentypen| | ValueInt | PlcTagInt -> short | | -| ✓ | OB_PCYCLE | Hardwaredatentypen| | ValueInt | PlcTagInt -> short | | -| ✓ | OB_STARTUP | Hardwaredatentypen| | ValueInt | PlcTagInt -> short | | -| ✓ | OB_TIMEERROR | Hardwaredatentypen| | ValueInt | PlcTagInt -> short | | -| ✓ | OB_TOD | Hardwaredatentypen| | ValueInt | PlcTagInt -> short | | -| ✓ | PIP | Hardwaredatentypen| | ValueUInt | PlcTagUInt -> ushort | | -| ✓ | Pointer | Zeiger | Parameter | ValueUSIntArray[6] | byte[6] | | -| ✓ | PORT | Hardwaredatentypen| | ValueUInt | PlcTagUInt -> ushort | | -| ✓ | RTM | Hardwaredatentypen| | ValueUInt | PlcTagUInt -> ushort | | -| ✓ | Real | Gleitpunktzahlen | | ValueReal | float | | -| ✓ | Remote | Zeiger | Parameter | ValueUSIntArray[10] | PlcTagAny -> byte[10] | Identisch zu Any-Pointer | -| ✓ | S5Time | Zeiten | | ValueWord | ushort, ushort | TODO: TimeBase, TimeValue. Vereinheitlichen? | -| ✓ | SInt | Ganzzahlen | | ValueSInt | sbyte | | -| ✓ | String | Zeichenfolgen | | ValueUSIntArray[stringlen + 2]| string | Encoding Voreinstellung ISO-8859-1 für non-ASCII | -| ✓ | Struct | | | | | Zugriff auf Einzelelemente direkt möglich | -| ✓ | Time | Zeiten | | ValueDInt | int | Anzahl ms mit Vorzeichen | -| ✓ | Time_Of_Day (TOD) | Datum und Uhrzeit | | ValueUDInt | uint | Anzahl ms seit 00:00:00 Uhr | -| ✓ | Timer | Parametertypen | Parameter | ValueUInt | PlcTagUInt -> ushort | | -| ✓ | UDInt | Ganzzahlen | | ValueUDInt | uint | | -| ✓ | UInt | Ganzzahlen | | ValueUInt | ushort | | -| ✓ | ULInt | Ganzzahlen | | ValueULInt | ulong | | -| ✓ | USInt | Ganzzahlen | | ValueUSInt | byte | | -| ✗ | Variant | Zeiger | Parameter | | | Erhält keine Adresse | -| ✓ | WChar | Zeichenfolgen | | ValueUInt | char | | -| ✓ | WString | Zeichenfolgen | | ValueUIntArray[stringlen + 2] | string | | -| ✓ | Word | Bitfolgen | | ValueWord | ushort | | - -## Lizenz - -Soweit nicht anders vermerkt, gilt für alle Quellcodes die GNU Lesser General Public License, -Version 3 oder später. +The public API for new applications is `S7CommPlusClient`. Low-level protocol/session types are internal implementation details; production code should use the client surface. + +## What The Production Client Provides + +- Async connect, disconnect, browse, read, write, active-alarm, subscription, block metadata, CPU metadata/control, online block-view, and legitimation methods +- Serialized request execution for safe concurrent callers +- Typed exceptions with PLC endpoint, operation, error code, and transient/non-transient classification +- Connection-state and communication-error events +- Read/browse reconnect retry support +- Explicit write enablement so accidental PLC writes are blocked by default +- Bounded disconnect behavior and better socket disconnect detection +- TLS and legacy S7CommPlus challenge authentication modes +- No library writes to `Console` + +## Requirements + +### PLC / CPU + +The default mode supports CPUs and projects that allow secure PG/HMI communication over TLS: + +- S7-1200 firmware V4.3 or newer, TLS 1.3 from V4.5 +- S7-1500 firmware V2.9 or newer +- Software controllers supported by the upstream protocol implementation + +The PLC project must also be configured with a TIA Portal version that supports secure communication, typically TIA Portal V17 or newer. + +For older S7-1200/1500 CPUs that do not support TLS, use `S7CommPlusSecurityMode.LegacyChallenge` or `Auto` on `net8.0`/`net9.0`. Legacy mode uses HarpoS7-derived challenge authentication and packet digests. `net6.0` builds remain TLS-only and fail fast if legacy mode is requested. + +### TLS Backend + +TLS communication uses the managed BouncyCastle backend by default. The older OpenSSL backend remains available through `S7CommPlusClientOptions.TlsBackend = S7CommPlusTlsBackend.OpenSsl`, but it depends on native runtime files and may be less portable across PLC firmware/OpenSSL combinations. + +The package includes the required native OpenSSL runtime files for supported platforms and copies them to the output directory when the OpenSSL backend is selected. If OpenSSL is installed system-wide, make sure the matching native binaries are available on the process path. + +Windows runtime files include: + +- `libcrypto-3.dll` / `libssl-3.dll` for x86 +- `libcrypto-3-x64.dll` / `libssl-3-x64.dll` for x64 +- `libcrypto-3-arm64.dll` / `libssl-3-arm64.dll` plus `vcruntime140.dll` for ARM64 + +## Installation + +Install the package from [NuGet](https://www.nuget.org/packages/DotNetProjects.S7CommPlusDriver): + +```powershell +dotnet add package DotNetProjects.S7CommPlusDriver +``` + +## Quick Start + +Create one `S7CommPlusClient` per PLC endpoint and reuse it for reads. The client serializes operations internally, so multiple callers can safely share the same instance. + +```csharp +await using var client = new S7CommPlusClient(new S7CommPlusClientOptions +{ + Address = "10.0.110.120", + RequestTimeout = TimeSpan.FromSeconds(5), + AutoReconnect = true +}); + +client.ConnectionStateChanged += (_, e) => +{ + Console.WriteLine($"{e.OldState} -> {e.NewState}"); +}; + +client.CommunicationError += (_, e) => +{ + Console.WriteLine($"{e.Exception.Operation}: 0x{e.Exception.ErrorCode:X8}"); +}; + +await client.ConnectAsync(); + +var cpuInfo = await client.GetCpuInfoAsync(); +var cpuCulture = await client.GetCpuCultureInfoAsync(); +var variables = await client.BrowseAsync(); + +var tag = await client.GetTagBySymbolAsync("MyDb.MyValue"); +var read = await client.ReadAsync(new[] { tag }); + +if (read.Items[0].IsSuccess) +{ + Console.WriteLine(tag.ToString()); +} + +await client.DisconnectAsync(); +``` + +`GetCpuCultureInfoAsync()` reads the CPU text-container LCIDs and exposes both +the raw language IDs and resolved .NET cultures: + +```csharp +foreach (var culture in cpuCulture.Cultures) +{ + Console.WriteLine($"{culture.LCID}: {culture.Name}"); +} +``` + +The built-in connection defaults match Siemens S7CommPlus HMI communication: +ISO-on-TCP port `S7CommPlusDefaults.IsoTcpPort` (`102`), local TSAP +`S7CommPlusDefaults.LocalTsap` (`0x0600`), and remote TSAP +`S7CommPlusDefaults.RemoteTsapHmi` (`SIMATIC-ROOT-HMI`). Project/engineering +captures sometimes use `S7CommPlusDefaults.RemoteTsapEs` +(`SIMATIC-ROOT-ES`). Remote TSAP values are validated as ASCII COTP +parameters before a socket is opened. + +## Password Legitimation + +Initial session authentication and PLC password legitimation are separate +steps. If the PLC requires a password for the desired access level, either pass +credentials in the options so they are used during connect, or call +`LegitimateAsync` after connecting: + +```csharp +await using var client = new S7CommPlusClient(new S7CommPlusClientOptions +{ + Address = "10.0.110.120", + Password = "plc-password" +}); + +await client.ConnectAsync(); + +// Or, for an already connected session: +await client.LegitimateAsync("plc-password", username: ""); +``` + +Legitimation is a session-security operation, not a PLC signal write, so it +does not require `WriteEnabled = true`. Failed legitimation attempts throw +`S7CommPlusLegitimationException`. + +## Active Alarms + +Active alarms are exposed through the same serialized read pipeline and can +therefore use the normal timeout, reconnect, logging, and typed-error behavior: + +```csharp +var alarms = await client.GetActiveAlarmsAsync(languageId: 1033); +``` + +Use this call to get alarms that are already active when your application +connects. Alarm subscriptions report new notification frames on that session; +they should not be used as the only source for pre-existing alarms. + +Each `S7CommPlusAlarm` exposes the decoded identity parts from `CpuAlarmId`: +`SourceRelationId` and `SourceAlarmId`. For PLC program alarms, +`SourceRelationId` is the PLC object relation id encoded into the alarm id; a +higher-level project/catalog API can map it to a block name or call path. + +## PLC Text Lists and Alarm Text Formatting + +PLC alarm texts can contain TIA-style text-list placeholders such as +`@2%t#519K@`. Load the PLC text-list catalog once and pass it to the alarm APIs +when you want those placeholders expanded with the same text lists that are +stored in the CPU: + +```csharp +var textLists = await client.GetTextListsAsync(); +var alarms = await client.GetActiveAlarmsAsync(languageId: 1031, textLists); + +foreach (var alarm in alarms) +{ + Console.WriteLine(alarm.AlarmTexts?.AlarmText); +} +``` + +`GetTextListsAsync()` discovers the CPU languages from the PLC text container +and loads all available localized text-list libraries, plus the +language-independent system text-list library. To load only selected languages, +pass LCIDs explicitly: + +```csharp +var textLists = await client.GetTextListsAsync(new[] { 1031, 2057 }); +``` + +The catalog remains a single API surface, but each `S7CommPlusTextList` exposes +`TextListType` so callers can distinguish runtime user lists from system lists: + +```csharp +var userLists = textLists.TextLists + .Where(list => list.TextListType == S7CommPlusTextListType.User); + +var systemLists = textLists.TextLists + .Where(list => list.TextListType == S7CommPlusTextListType.System); +``` + +The online PLC payload does not expose the full engineering project object +kind, so the driver classifies text lists by Siemens runtime list-id +conventions. The +catalog resolver still uses all lists, because system diagnostic texts and user +alarm texts can reference each other recursively. + +Associated-value placeholders are formatted as well. For example, `@2W%d@` +uses the second associated value as a `WORD` and formats it as signed decimal, +matching TIA's standard element-type syntax. + +## Communication Limits + +PLC communication limits are available through the production client. This is +useful before creating subscriptions or planning large batch reads: + +```csharp +var resources = await client.GetCommunicationResourcesAsync(); + +Console.WriteLine(resources.TagsPerReadRequestMax); +Console.WriteLine(resources.PlcSubscriptionsFree); +``` + +## Block Metadata and Online View + +The production client can browse block metadata, parse the PLC structure XML +into a block tree, read block content, and open TIA-style block online-view +subscriptions: + +```csharp +var blocks = await client.BrowseBlocksAsync(); +var structure = await client.BrowseBlockStructureAsync(); +var content = await client.GetBlockContentAsync(blocks[0].RelationId); +``` + +`OpenBlockOnlineViewAsync` creates a disposable `S7CommPlusTisWatchSubscription` +for advanced block-watch scenarios. This API is lower level than tag reads and +requires a caller-provided `S7CommPlusTisWatchRequest` that matches the block +watch points and result model. + +## Subscriptions + +Subscriptions are exposed as long-running, disposable objects. They own the +client operation pipeline while active because notification frames arrive on the +same PLC session as normal request/response traffic. Stop or dispose a +subscription before issuing other reads or writes on the same client. + +```csharp +var tag = await client.GetTagBySymbolAsync("MyDb.MyValue"); + +await using var subscription = await client.SubscribeTagsAsync( + new[] { tag }, + new S7CommPlusSubscriptionOptions + { + CycleTimeMilliseconds = 250, + NotificationTimeout = TimeSpan.FromSeconds(5) + }); + +subscription.NotificationReceived += (_, e) => +{ + foreach (var item in e.Notification.Items) + { + if (item.IsSuccess) + { + Console.WriteLine(item.Tag); + } + } +}; + +subscription.CommunicationError += (_, e) => +{ + Console.WriteLine($"{e.Exception.Operation}: 0x{e.Exception.ErrorCode:X8}"); +}; +``` + +Alarm notifications use the same lifecycle and credit handling: + +```csharp +await using var snapshotClient = new S7CommPlusClient(options); +var textLists = await client.GetTextListsAsync(new[] { 1033 }); +await using var alarmSession = await client.SubscribeAlarmsWithSnapshotAsync(snapshotClient, 1033, textLists); + +foreach (var activeAlarm in alarmSession.ActiveAlarms) +{ + Console.WriteLine(activeAlarm.AlarmTexts?.AlarmText); +} + +alarmSession.Subscription.NotificationReceived += (_, e) => +{ + foreach (var alarm in e.Notification.Alarms) + { + Console.WriteLine(alarm.ToString()); + } +}; +``` + +Subscriptions can share one `S7CommPlusClient` with reads, metadata calls, and +other subscriptions. The client keeps one foreground request in flight per +physical PLC connection and routes notifications by PLC subscription object id. +Use a second client only when you explicitly want a second physical PLC +connection, for example for true parallel large metadata transfers. + +Use `SubscribeAlarmsAsync()` or `GetActiveAlarmsAsync()` without a language id +to request every alarm text language returned by the PLC. The compatibility +`AlarmTexts` property contains the selected or first returned language, while +`AlarmTextsByLanguage` contains the full set. + +Alarm snapshots require explicit connection ownership: create another +`S7CommPlusClient` and pass it to `SubscribeAlarmsWithSnapshotAsync` when you +want a live alarm subscription plus an initial active-alarm snapshot on a +separate physical connection. Stopping an alarm subscription deletes only that +subscription and keeps the client session available for later requests. + +Idle notification waits are not treated as failures by default. Set +`MaxConsecutiveTimeoutsBeforeFault` when a quiet subscription should become a +typed communication failure after a fixed number of empty waits. Write +protection is unchanged: subscriptions do not enable PLC signal writes. + +## Older PLCs / Legacy Challenge Auth + +TLS remains the default to avoid accidental security downgrades. Enable the older Siemens challenge authentication explicitly: + +```csharp +await using var client = new S7CommPlusClient(new S7CommPlusClientOptions +{ + Address = "10.0.110.120", + SecurityMode = S7CommPlusSecurityMode.LegacyChallenge, + RequestTimeout = TimeSpan.FromSeconds(5) +}); + +await client.ConnectAsync(); +Console.WriteLine(client.Options.NegotiatedSecurityMode); +``` + +`S7CommPlusSecurityMode.Auto` tries TLS first and then falls back to legacy challenge authentication. Reconnect and write safety behave the same in all modes: read/browse may reconnect once, writes are never retried automatically, and writes still require `WriteEnabled = true`. + +Legacy challenge support references the `HarpoS7` and `HarpoS7.PublicKeys` +NuGet packages on `net8.0` and `net9.0`. HarpoS7 is MIT licensed; this project +remains LGPL-3.0-or-later unless noted otherwise. + +Legacy packet-capture notes, auth frame variants, and live PLC observations are tracked in `docs/legacy-s7commplus-memory.md`. + +Recent legacy auth work parses `ServerSessionVersion` from the CreateObject +response where available, so S7-1500 V3.x authentication-frame selection is +response-driven before falling back to capture-compatible heuristics. + +## Write Safety + +Writes are disabled by default. This is intentional for production services and tests. + +```csharp +await using var client = new S7CommPlusClient(new S7CommPlusClientOptions +{ + Address = "10.0.110.120", + WriteEnabled = true +}); + +await client.ConnectAsync(); +await client.WriteAsync(new[] { tag }); +``` + +If `WriteEnabled` is `false`, write calls throw `S7CommPlusWriteDisabledException`. + +## Error Handling + +Operations throw `S7CommPlusException` subclasses instead of returning only integer error codes. The exception includes: + +- `Operation` +- `Endpoint` +- `ErrorCode` +- `IsTransient` + +Read and browse operations retry once after a transient communication failure when `AutoReconnect` is enabled. Writes are not retried automatically. + +## Logging And Diagnostics + +`S7CommPlusClientOptions.Logger` accepts an `ILogger`. If no logger is provided, `NullLogger` is used. + +The library no longer writes diagnostics to `Console`. Lower-level diagnostics are written via `System.Diagnostics.Trace`. + +TLS key logging for Wireshark analysis is still available through the low-level `S7Client.WriteSslKeyToFile` and `S7Client.WriteSslKeyPath` settings. Use this only in controlled diagnostic environments. + +## Testing + +Run the normal build and unit tests: + +```powershell +dotnet build src\S7CommPlusDriver.slnx /nodeReuse:false +dotnet test src\S7CommPlusDriver.Tests\S7CommPlusDriver.Tests.csproj --no-restore /nodeReuse:false +``` + +The live PLC smoke test is opt-in and read-only by default: + +```powershell +$env:S7COMMPLUS_LIVE_HOST = "10.0.110.120" +dotnet test src\S7CommPlusDriver.Tests\S7CommPlusDriver.Tests.csproj --filter LivePlcReadOnlySmokeTest +``` + +By default the live test only connects, reads CPU info, browses, and disconnects. To read explicit tags, provide semicolon-separated tag symbols: + +```powershell +$env:S7COMMPLUS_LIVE_TAGS = "MyDb.MyValue;OtherDb.Counter" +``` + +To exercise legacy auth or auto fallback in the live test: + +```powershell +$env:S7COMMPLUS_LIVE_SECURITY_MODE = "LegacyChallenge" # or "Auto" +``` + +Never use the live smoke test for writes. + +## Tested Communication + +Known tested targets include: + +- S7 1211 firmware V4.5 +- TIA PLCSIM V17 with NetToPLCSim +- TIA PLCSIM V18 with NetToPLCSim +- read-only smoke test against a real PLC at `10.0.110.120` + +## Supported Data Types + +The `PlcTag` classes convert PLC values into .NET-friendly types. Supported data types include scalar and array variants for common Siemens types such as `Bool`, `Byte`, `Word`, `Int`, `DInt`, `Real`, `LReal`, `String`, `WString`, `Date`, `Date_And_Time`, `DTL`, `Time`, `LTime`, pointer-like types, hardware identifiers, counters, and timers. + +For exact mappings, see the implementations in `src/S7CommPlusDriver/ClientApi/PlcTag.cs` and `src/S7CommPlusDriver/ClientApi/PlcTags.cs`. + +## License + +Unless otherwise noted, all source code is licensed under LGPL-3.0-or-later. ## Authors -* **Thomas Wiens** - *Initial work* - [thomas-v2](https://github.com/thomas-v2) +- Thomas Wiens - initial work - https://github.com/thomas-v2 +- DotNetProjects contributors diff --git a/docs/online-multiplexing-todo.md b/docs/online-multiplexing-todo.md new file mode 100644 index 0000000..69c3529 --- /dev/null +++ b/docs/online-multiplexing-todo.md @@ -0,0 +1,40 @@ +# S7CommPlus online multiplexing notes + +Status: implemented in the driver refactor. One `S7CommPlusClient` owns exactly one PLC connection. The library must not silently open additional PLC connections. If an application wants more physical PLC connections, it creates more clients explicitly. + +Observed TIA behavior in `C:\Data\temp\Online CPU FA01.pcapng`: + +- TIA opened two ES connections to the PLC, not one connection per watched block. +- Online block view traffic used one long-lived watch connection. +- Additional opened or expanded blocks created additional TIS jobs/subscriptions on that same watch connection. +- Responses and notifications were interleaved; notifications are routed by subscription object id. + +Implemented driver changes: + +- Added a receive dispatcher per protocol session, but arbitrary PLC requests are not run in parallel. + - Thomas' 2025 note is correct for fragmented response bodies: once a large S7CommPlus response is fragmented, the individual fragments do not carry the request sequence number. Sending multiple fragment-producing requests concurrently can make fragment ownership ambiguous. + - One foreground request stays in flight per physical S7CommPlus connection. + - Responses are matched by function code and sequence number. + - Notifications are routed by `Notification.SubscriptionObjectId`. + - Alarm part-2 notifications are also routed by `Notification.P2SubscriptionObjectId` when present. + - Consumed notifications are not left in the dispatcher queue for a second delivery. +- Public API rule: + - One `S7CommPlusClient` means one PLC connection. + - Multiple physical connections are created only when the application creates multiple clients. +- Replaced singleton subscription state with per-subscription handles. + - Tags: subscription object id, reference-id-to-tag map, next credit limit. + - Alarms: subscription object id, next credit limit. + - TIS watch: job object id, subscription object id, subscription-ref object id, result model, poll counter. +- Multiple subscriptions can coexist on one client. + - Create/delete/read/metadata operations stay serialized as normal session requests. + - Notification loops wait for their own subscription object id. + - Reads/metadata may run while subscriptions are active, but not in parallel with another foreground request on the same physical connection. + - If an application needs true parallel large metadata/block transfers, it should create a second `S7CommPlusClient` explicitly. +- Explicit helper APIs stay explicit. + - Snapshot helpers may accept another `S7CommPlusClient`, but should not create one implicitly. +- For online engineering features prefer `S7CommPlusDefaults.RemoteTsapEs`. + +Live-test rule: + +- Do not test against `10.0.10.100` or the production/FA01 CPU. +- If live PLC testing is needed, use only `10.0.98.206`. diff --git a/src/.gitignore b/src/.gitignore index ee97264..28cce75 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -11,5 +11,4 @@ [Rr]elease/ [Bb]in/ [Oo]bj/ -x64/ build/ diff --git a/src/DriverTest/DriverTest.csproj b/src/DriverTest/DriverTest.csproj index 3dad8a5..a337fa3 100644 --- a/src/DriverTest/DriverTest.csproj +++ b/src/DriverTest/DriverTest.csproj @@ -1,92 +1,26 @@ - - - - - Debug - AnyCPU - {94755B6A-7E0D-40F9-A2ED-48FF0C0FEA2B} - Exe - Properties - DriverTest - DriverTest - v4.7.2 - 512 - true - - - - true - bin\x64\Debug\ - DEBUG;TRACE - full - x64 - 7.3 - prompt - true - - - bin\x64\Release\ - TRACE - true - pdbonly - x64 - 7.3 - prompt - true - - - true - bin\x86\Debug\ - DEBUG;TRACE - full - x86 - 7.3 - prompt - true - - - bin\x86\Release\ - TRACE - true - pdbonly - x86 - 7.3 - prompt - true - - - - - - - - - - - - - - - - - - - - - - {781951d8-80de-4e2f-92ca-a5fef0059415} - S7CommPlusDriver - - - - - xcopy /y /d "$(SolutionDir)S7CommPlusDriver\OpenSSL-dll-$(PlatformName)\*.dll" "$(ProjectDir)$(OutDir)" - - + + + net8.0 + Exe + false + + + + + + + + + + + + + DriverTest + S7CommPlusDriver Test + DriverTest + Copyright © 2023, Thomas Wiens + 0.1.0.0 + 0.1.0.0 + x86 + \ No newline at end of file diff --git a/src/DriverTest/Program.cs b/src/DriverTest/Program.cs index 7be3757..00d5b78 100644 --- a/src/DriverTest/Program.cs +++ b/src/DriverTest/Program.cs @@ -1,180 +1,95 @@ -//#define _TEST_BASIC_VAR -#define _TEST_PLCTAG - -using System; -using System.Collections.Generic; using S7CommPlusDriver; - using S7CommPlusDriver.ClientApi; - +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; namespace DriverTest { - class Program + internal static class Program { - static void Main(string[] args) + private static async Task Main(string[] args) { - string HostIp = "192.168.1.30"; - string Password = ""; - string Username = ""; - int res; - List readlist = new List(); + var hostIp = args.Length >= 1 ? args[0] : "10.0.98.100"; + var password = args.Length >= 2 ? args[1] : string.Empty; + var username = args.Length >= 3 ? args[2] : string.Empty; + Console.WriteLine("Main - START"); - // Als Parameter lässt sich die IP-Adresse übergeben, sonst Default-Wert von oben - if (args.Length >= 1) + Console.WriteLine("Main - connecting to: " + hostIp); + + var options = new S7CommPlusClientOptions { - HostIp = args[0]; - } - // Als Parameter lässt sich das Passwort übergeben, sonst Default-Wert von oben (kein Passwort) - if (args.Length >= 2) + Address = hostIp, + Password = password, + Username = username, + WriteEnabled = true + }; + + await using var client = new S7CommPlusClient(options); + try { - Password = args[1]; + await client.ConnectAsync().ConfigureAwait(false); + Console.WriteLine("Main - connected"); + + Console.WriteLine("Main - browsing variables..."); + var vars = await client.BrowseAsync().ConfigureAwait(false); + Console.WriteLine("Main - browse count=" + vars.Count); + + await ReadBrowsableTagsAsync(client, vars).ConfigureAwait(false); + + // The full typed tag write/read regression test is intentionally disabled by default. + // It writes PLC memory and therefore requires a matching test project and WriteEnabled=true. + // var test = new TestPlcTag(); + // var errors = test.DoTests(client, nrandom: 10, testPointers: false); + // Console.WriteLine("TestPlcTag errors=" + errors); } - // Als Parameter lässt sich der Username übergeben, sonst Default-Wert von oben (kein Username) - if (args.Length >= 3) + catch (S7CommPlusException ex) { - Username = args[2]; + Console.WriteLine($"Main - PLC communication failed: {ex.Message} ({ex.ErrorCode})"); } - Console.WriteLine("Main - Versuche Verbindungsaufbau zu: " + HostIp); - - S7CommPlusConnection conn = new S7CommPlusConnection(); - res = conn.Connect(HostIp, Password, Username); - if (res == 0) + finally { - Console.WriteLine("Main - Connect fertig"); - - #region Variablenhaushalt browsen - Console.WriteLine("Main - Starte Browse..."); - // Variablenhaushalt auslesen - List vars = new List(); - res = conn.Browse(out vars); - Console.WriteLine("Main - Browse res=" + res); - #endregion - -#if _TEST_PLCTAG - #region Werte aller Variablen einlesen - Console.WriteLine("Main - Lese Werte aller Variablen aus"); - - List taglist = new List(); - var tags = new List(); - - foreach (var v in vars) - { - taglist.Add(PlcTags.TagFactory(v.Name, new ItemAddress(v.AccessSequence), v.Softdatatype)); - } - foreach (var t in taglist) - { - tags.Add(t); - } - res = conn.ReadTags(tags); - if (res == 0) - { - Console.WriteLine("====================== VARIABLENHAUSHALT ======================"); - - string formatstring = "{0,-80}{1,-30}{2,-20}{3,-20}"; - Console.WriteLine(String.Format(formatstring, "SYMBOLIC-NAME", "ACCESS-SEQUENCE", "TYP", "QC: VALUE")); - for (int i = 0; i < vars.Count; i++) - { - string s; + await client.DisconnectAsync().ConfigureAwait(false); + } - s = String.Format(formatstring, taglist[i].Name, taglist[i].Address.GetAccessString(), Softdatatype.Types[taglist[i].Datatype], taglist[i].ToString()); - Console.WriteLine(s); - } - } - #endregion -#endif + Console.WriteLine("Main - END. Press any key."); + Console.ReadKey(); + } -#if _TEST_BASIC_VAR - #region Werte aller Variablen einlesen - Console.WriteLine("Main - Lese Werte aller Variablen aus"); + private static async Task ReadBrowsableTagsAsync(S7CommPlusClient client, IReadOnlyList vars) + { + Console.WriteLine("Main - reading variable values"); - foreach (var v in vars) + var tags = new List(); + foreach (var variable in vars) + { + try { - readlist.Add(new ItemAddress(v.AccessSequence)); + tags.Add(await client.GetTagBySymbolAsync(variable.Name).ConfigureAwait(false)); } - List values = new List(); - List errors = new List(); - - - // Fehlerhafte Variable setzen - //readlist[2].LID[0] = 123; - res = conn.ReadValues(readlist, out values, out errors); - #endregion - - - #region Variablenhaushalt mit Werten ausgeben - - if (res == 0) + catch (S7CommPlusException ex) { - Console.WriteLine("====================== VARIABLENHAUSHALT ======================"); - - // Liste ausgeben - string formatstring = "{0,-80}{1,-30}{2,-20}{3,-20}"; - Console.WriteLine(String.Format(formatstring, "SYMBOLIC-NAME", "ACCESS-SEQUENCE", "TYP", "VALUE")); - for (int i = 0; i < vars.Count; i++) - { - string s = String.Format(formatstring, vars[i].Name, vars[i].AccessSequence, Softdatatype.Types[vars[i].Softdatatype], values[i]); - Console.WriteLine(s); - } - Console.WriteLine("==============================================================="); + Console.WriteLine($"Skipping {variable.Name}: {ex.Message}"); } - #endregion -#endif - - /* - #region Test: Wert schreiben - List writevalues = new List(); - PValue writeValue = new ValueInt(8888); - writevalues.Add(writeValue); - List writelist = new List(); - writelist.Add(new ItemAddress("8A0E0001.F")); - errors.Clear(); - res = conn.WriteValues(writelist, writevalues, out errors); - Console.WriteLine("res=" + res); - #endregion - */ - - /* - #region Test: Absolutadressen lesen - // Daten aus nicht "optimierten" Datenbausteinen lesen - readlist.Clear(); - ItemAddress absAdr = new ItemAddress(); - absAdr.SetAccessAreaToDatablock(100); // DB 100 - absAdr.SymbolCrc = 0; - - absAdr.AccessSubArea = Ids.DB_ValueActual; - absAdr.LID.Add(3); // LID_OMS_STB_ClassicBlob - absAdr.LID.Add(0); // Blob Start Offset, Anfangsadresse - absAdr.LID.Add(20); // 20 Bytes - - readlist.Add(absAdr); + } - values.Clear(); - errors.Clear(); + if (tags.Count == 0) + { + Console.WriteLine("No readable tags found."); + return; + } - res = conn.ReadValues(readlist, out values, out errors); - Console.WriteLine(values.ToString()); - #endregion - */ + await client.ReadAsync(tags).ConfigureAwait(false); + Console.WriteLine("====================== VARIABLES ======================"); - /* - #region Test: SPS in Stopp setzen - Console.WriteLine("Setze SPS in STOP..."); - conn.SetPlcOperatingState(1); - Console.WriteLine("Taste drücken um wieder in RUN zu setzen..."); - Console.ReadKey(); - Console.WriteLine("Setze SPS in RUN..."); - conn.SetPlcOperatingState(3); - #endregion - */ - conn.Disconnect(); - } - else + const string format = "{0,-80}{1,-30}{2,-20}{3,-20}"; + Console.WriteLine(string.Format(format, "SYMBOLIC-NAME", "ACCESS-SEQUENCE", "TYPE", "QC: VALUE")); + foreach (var tag in tags) { - Console.WriteLine("Main - Connect fehlgeschlagen!"); + var datatype = Softdatatype.Types.TryGetValue(tag.Datatype, out var name) ? name : tag.Datatype.ToString(); + Console.WriteLine(string.Format(format, tag.Name, tag.Address.GetAccessString(), datatype, tag.ToString())); } - Console.WriteLine("Main - ENDE. Bitte Taste drücken."); - Console.ReadKey(); } } } diff --git a/src/DriverTest/Properties/AssemblyInfo.cs b/src/DriverTest/Properties/AssemblyInfo.cs deleted file mode 100644 index 0c4d45b..0000000 --- a/src/DriverTest/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("DriverTest")] -[assembly: AssemblyDescription("S7CommPlusDriver Test")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DriverTest")] -[assembly: AssemblyCopyright("Copyright © 2023, Thomas Wiens")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("94755b6a-7e0d-40f9-a2ed-48ff0c0fea2b")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.1.0.0")] -[assembly: AssemblyFileVersion("0.1.0.0")] diff --git a/src/DriverTest/TestPlcTag.cs b/src/DriverTest/TestPlcTag.cs index 32b06b7..f5bced6 100644 --- a/src/DriverTest/TestPlcTag.cs +++ b/src/DriverTest/TestPlcTag.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -21,7 +21,7 @@ namespace DriverTest { - public class TestPlcTag + internal class TestPlcTag { // If the DB is created from source, the .x IDs should // be remain the same, if there's no change in the DB at the variables after compiling. @@ -61,6 +61,32 @@ public class TestPlcTag private Random m_Rand; + private static int ReadTags(S7CommPlusClient client, IEnumerable tags) + { + try + { + client.ReadAsync(tags).GetAwaiter().GetResult(); + return 0; + } + catch (S7CommPlusException ex) + { + return ex.ErrorCode; + } + } + + private static int WriteTags(S7CommPlusClient client, IEnumerable tags) + { + try + { + client.WriteAsync(tags).GetAwaiter().GetResult(); + return 0; + } + catch (S7CommPlusException ex) + { + return ex.ErrorCode; + } + } + /// /// Start the testrun for the PlcTag types /// @@ -68,7 +94,7 @@ public class TestPlcTag /// Number of random values to generate for each Tag type /// Option to do the testruns on Pointer types (Any, Pointer) /// - public int DoTests(S7CommPlusConnection conn, int nrandom, bool testPointers) + public int DoTests(S7CommPlusClient conn, int nrandom, bool testPointers) { int result = 0; m_Rand = new Random(); @@ -123,7 +149,7 @@ public int DoTests(S7CommPlusConnection conn, int nrandom, bool testPointers) } #region Tests for Boolean tags - private int Test_PlcTag_Bool(S7CommPlusConnection conn) + private int Test_PlcTag_Bool(S7CommPlusClient conn) { int result = 0; Console.WriteLine("*** TestPlcTag_Bool ***"); @@ -133,13 +159,13 @@ private int Test_PlcTag_Bool(S7CommPlusConnection conn) tags.Add(tag); for (int i = 0; i < 5; i++) { - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); writevalue = !(tag.Value); tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); if (tag.Value == writevalue) { @@ -157,7 +183,7 @@ private int Test_PlcTag_Bool(S7CommPlusConnection conn) #endregion #region Tests for Bitstring tags - private int Test_PlcTag_Byte(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_Byte(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_Byte ***"); @@ -172,14 +198,14 @@ private int Test_PlcTag_Byte(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = 0x{1:X2}", tag.Name, tag.Value)); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = 0x{1:X2}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = 0x{1:X2}", tag.Name, tag.Value)); if (tag.Value == writevalue) { @@ -195,7 +221,7 @@ private int Test_PlcTag_Byte(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_Word(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_Word(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_Word ***"); @@ -211,14 +237,14 @@ private int Test_PlcTag_Word(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = 0x{1:X4}", tag.Name, tag.Value)); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = 0x{1:X4}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = 0x{1:X4}", tag.Name, tag.Value)); if (tag.Value == writevalue) { @@ -234,7 +260,7 @@ private int Test_PlcTag_Word(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_DWord(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_DWord(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_DWord ***"); @@ -249,14 +275,14 @@ private int Test_PlcTag_DWord(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = 0x{1:X8}", tag.Name, tag.Value)); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = 0x{1:X8}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = 0x{1:X8}", tag.Name, tag.Value)); if (tag.Value == writevalue) { @@ -272,7 +298,7 @@ private int Test_PlcTag_DWord(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_LWord(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_LWord(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_LWord ***"); @@ -287,14 +313,14 @@ private int Test_PlcTag_LWord(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = 0x{1:X8}", tag.Name, tag.Value)); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = 0x{1:X8}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = 0x{1:X8}", tag.Name, tag.Value)); if (tag.Value == writevalue) { @@ -312,7 +338,7 @@ private int Test_PlcTag_LWord(S7CommPlusConnection conn, int nrandom) #endregion #region Tests for Integer tags - private int Test_PlcTag_USInt(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_USInt(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_USInt ***"); @@ -327,14 +353,14 @@ private int Test_PlcTag_USInt(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); if (tag.Value == writevalue) { @@ -350,7 +376,7 @@ private int Test_PlcTag_USInt(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_SInt(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_SInt(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_SInt ***"); @@ -365,14 +391,14 @@ private int Test_PlcTag_SInt(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); if (tag.Value == writevalue) { @@ -388,7 +414,7 @@ private int Test_PlcTag_SInt(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_UInt(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_UInt(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_UInt ***"); @@ -403,14 +429,14 @@ private int Test_PlcTag_UInt(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); if (tag.Value == writevalue) { @@ -426,7 +452,7 @@ private int Test_PlcTag_UInt(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_Int(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_Int(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_Int ***"); @@ -441,14 +467,14 @@ private int Test_PlcTag_Int(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); if (tag.Value == writevalue) { @@ -464,7 +490,7 @@ private int Test_PlcTag_Int(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_UDInt(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_UDInt(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_UDInt ***"); @@ -479,14 +505,14 @@ private int Test_PlcTag_UDInt(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); if (tag.Value == writevalue) { @@ -502,7 +528,7 @@ private int Test_PlcTag_UDInt(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_DInt(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_DInt(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_DInt ***"); @@ -517,14 +543,14 @@ private int Test_PlcTag_DInt(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); if (tag.Value == writevalue) { @@ -540,7 +566,7 @@ private int Test_PlcTag_DInt(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_ULInt(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_ULInt(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_ULInt ***"); @@ -555,14 +581,14 @@ private int Test_PlcTag_ULInt(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); if (tag.Value == writevalue) { @@ -578,7 +604,7 @@ private int Test_PlcTag_ULInt(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_LInt(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_LInt(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_LInt ***"); @@ -593,14 +619,14 @@ private int Test_PlcTag_LInt(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); if (tag.Value == writevalue) { @@ -618,7 +644,7 @@ private int Test_PlcTag_LInt(S7CommPlusConnection conn, int nrandom) #endregion #region Tests for Floating point tags - private int Test_PlcTag_Real(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_Real(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_Real ***"); @@ -636,14 +662,14 @@ private int Test_PlcTag_Real(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); if (tag.Value == writevalue) { @@ -659,7 +685,7 @@ private int Test_PlcTag_Real(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_LReal(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_LReal(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_LReal ***"); @@ -675,14 +701,14 @@ private int Test_PlcTag_LReal(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); if (tag.Value == writevalue) { @@ -700,7 +726,7 @@ private int Test_PlcTag_LReal(S7CommPlusConnection conn, int nrandom) #endregion #region Tests for Time value tags - private int Test_PlcTag_Time(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_Time(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_Time ***"); @@ -715,14 +741,14 @@ private int Test_PlcTag_Time(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1,-11} - as String {2}", tag.Name, tag.Value, tag.ToString())); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1,-11}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1,-11} - as String {2}", tag.Name, tag.Value, tag.ToString())); if (tag.Value == writevalue) { @@ -738,7 +764,7 @@ private int Test_PlcTag_Time(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_LTime(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_LTime(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_LTime ***"); @@ -753,14 +779,14 @@ private int Test_PlcTag_LTime(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1,-22} - as String {2}", tag.Name, tag.Value, tag.ToString())); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1,-22}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1,-22} - as String {2}", tag.Name, tag.Value, tag.ToString())); if (tag.Value == writevalue) { @@ -776,7 +802,7 @@ private int Test_PlcTag_LTime(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_S5Time(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_S5Time(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_S5Time ***"); @@ -792,15 +818,15 @@ private int Test_PlcTag_S5Time(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value (TimeValue, TimeBase) = ({1,-3}, {2,-3}) -> {3,-5}", tag.Name, tag.TimeValue, tag.TimeBase, tag.ToString())); foreach (var writevalue in testvalues) { tag.TimeValue = writevalue.TimeValue; tag.TimeBase = writevalue.TimeBase; Console.WriteLine(String.Format("Name= {0,-20}: Write value (TimeValue, TimeBase) = ({1,-3}, {2,-3})", tag.Name, writevalue.TimeValue, writevalue.TimeBase)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value (TimeValue, TimeBase) = ({1,-3}, {2,-3}) -> {3,-5}", tag.Name, tag.TimeValue, tag.TimeBase, tag.ToString())); if (tag.TimeValue == writevalue.TimeValue && tag.TimeBase == writevalue.TimeBase) { @@ -818,7 +844,7 @@ private int Test_PlcTag_S5Time(S7CommPlusConnection conn, int nrandom) #endregion #region Tests for Date and Time value tags - private int Test_PlcTag_Date(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_Date(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_Date ***"); @@ -833,14 +859,14 @@ private int Test_PlcTag_Date(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value.ToShortDateString())); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1}", tag.Name, writevalue.ToShortDateString())); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value.ToShortDateString())); if (tag.Value == writevalue) { @@ -856,7 +882,7 @@ private int Test_PlcTag_Date(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_DateAndTime(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_DateAndTime(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_DateAndTime ***"); @@ -873,14 +899,14 @@ private int Test_PlcTag_DateAndTime(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}.{2:D03}", tag.Name, tag.Value, tag.Value.Millisecond)); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1}.{2:D03}", tag.Name, writevalue, writevalue.Millisecond)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}.{2:D03}", tag.Name, tag.Value, tag.Value.Millisecond)); if (tag.Value == writevalue) { @@ -896,7 +922,7 @@ private int Test_PlcTag_DateAndTime(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_LDT(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_LDT(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_LDT ***"); @@ -911,14 +937,14 @@ private int Test_PlcTag_LDT(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1,-20} - as String {2}", tag.Name, tag.Value, tag.ToString())); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1,-20}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1,-20} - as String {2}", tag.Name, tag.Value, tag.ToString())); if (tag.Value == writevalue) { @@ -934,7 +960,7 @@ private int Test_PlcTag_LDT(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_TimeOfDay(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_TimeOfDay(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_TimeOfDay ***"); @@ -949,14 +975,14 @@ private int Test_PlcTag_TimeOfDay(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1,-20} - as String {2}", tag.Name, tag.Value, tag.ToString())); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1,-20}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1,-20} - as String {2}", tag.Name, tag.Value, tag.ToString())); if (tag.Value == writevalue) { @@ -972,7 +998,7 @@ private int Test_PlcTag_TimeOfDay(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_LTOD(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_LTOD(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_LTOD ***"); @@ -987,14 +1013,14 @@ private int Test_PlcTag_LTOD(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1,-20} - as String {2}", tag.Name, tag.Value, tag.ToString())); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1,-20}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1,-20} - as String {2}", tag.Name, tag.Value, tag.ToString())); if (tag.Value == writevalue) { @@ -1010,7 +1036,7 @@ private int Test_PlcTag_LTOD(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_DTL(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_DTL(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_DTL ***"); @@ -1036,15 +1062,15 @@ private int Test_PlcTag_DTL(S7CommPlusConnection conn, int nrandom) Console.WriteLine("DTLInterfaceTimestamp=" + tag.DTLInterfaceTimestamp); tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}.{2:D09}", tag.Name, tag.Value, tag.ValueNanosecond)); foreach (var writevalue in testvalues) { tag.Value = writevalue.dt; tag.ValueNanosecond = writevalue.ns; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1}.{2:D09}", tag.Name, writevalue.dt, writevalue.ns)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}.{2:D09}", tag.Name, tag.Value, tag.ValueNanosecond)); if (tag.Value == writevalue.dt && tag.ValueNanosecond == writevalue.ns) { @@ -1062,13 +1088,13 @@ private int Test_PlcTag_DTL(S7CommPlusConnection conn, int nrandom) #endregion #region Tests for Character / String tags - private int Test_PlcTag_Char(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_Char(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_Char ***"); var tags = new List(); var tag = new PlcTagChar("Char_Var", new ItemAddress(PlcTagCharAddress), Softdatatype.S7COMMP_SOFTDATATYPE_CHAR); - var testvalues = new List() { 'A', 'B', ' ', 'ä', '1' }; + var testvalues = new List() { 'A', 'B', ' ', 'ä', '1' }; int cval; for (int i = 0; i < nrandom; i++) @@ -1078,15 +1104,15 @@ private int Test_PlcTag_Char(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); if (tag.Value == writevalue) { @@ -1102,13 +1128,13 @@ private int Test_PlcTag_Char(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_WChar(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_WChar(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_WChar ***"); var tags = new List(); var tag = new PlcTagWChar("WChar_Var", new ItemAddress(PlcTagWCharAddress), Softdatatype.S7COMMP_SOFTDATATYPE_WCHAR); - var testvalues = new List() { 'A', 'B', ' ', 'ä', '1', 'Æ©' }; + var testvalues = new List() { 'A', 'B', ' ', 'ä', '1', '?' }; int cval; for (int i = 0; i < nrandom; i++) @@ -1118,15 +1144,15 @@ private int Test_PlcTag_WChar(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); if (tag.Value == writevalue) { @@ -1142,14 +1168,14 @@ private int Test_PlcTag_WChar(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_String(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_String(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_String ***"); byte string_maxlength = 254; var tags = new List(); var tag = new PlcTagString("String_Var", new ItemAddress(PlcTagStringAddress), Softdatatype.S7COMMP_SOFTDATATYPE_STRING, string_maxlength); - var testvalues = new List() { "Hello", "World", "This is a test!", "Motörhead", "abcdefghijklmnopqrstuvwxyz1234567890" }; + var testvalues = new List() { "Hello", "World", "This is a test!", "Motörhead", "abcdefghijklmnopqrstuvwxyz1234567890" }; // Generate a string of maxlength string smax = String.Empty; @@ -1164,15 +1190,15 @@ private int Test_PlcTag_String(S7CommPlusConnection conn, int nrandom) testvalues.Add(smax); tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); if (tag.Value == writevalue) { @@ -1188,14 +1214,14 @@ private int Test_PlcTag_String(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_WString(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_WString(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_WString ***"); byte string_maxlength = 254; var tags = new List(); var tag = new PlcTagWString("WString_Var", new ItemAddress(PlcTagWStringAddress), Softdatatype.S7COMMP_SOFTDATATYPE_WSTRING, string_maxlength); - var testvalues = new List() { "Hello", "World", "This is a test!", "Motörhead", "Test Greek ΣΛΔ end." }; + var testvalues = new List() { "Hello", "World", "This is a test!", "Motörhead", "Test Greek S?? end." }; // Generate a string of maxlength string smax = String.Empty; @@ -1210,15 +1236,15 @@ private int Test_PlcTag_WString(S7CommPlusConnection conn, int nrandom) testvalues.Add(smax); tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1}", tag.Name, writevalue)); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1}", tag.Name, tag.Value)); if (tag.Value == writevalue) { @@ -1236,7 +1262,7 @@ private int Test_PlcTag_WString(S7CommPlusConnection conn, int nrandom) #endregion #region Tests for Pointers type tags - private int Test_PlcTag_Pointer(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_Pointer(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_Pointer ***"); @@ -1266,14 +1292,14 @@ private int Test_PlcTag_Pointer(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1} {2}", tag.Name, GetHexstring(tag.Value), tag.ToString())); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1}", tag.Name, GetHexstring(writevalue))); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1} {2}", tag.Name, GetHexstring(tag.Value), tag.ToString())); if (tag.Value.SequenceEqual(writevalue)) { @@ -1289,7 +1315,7 @@ private int Test_PlcTag_Pointer(S7CommPlusConnection conn, int nrandom) return result; } - private int Test_PlcTag_Any(S7CommPlusConnection conn, int nrandom) + private int Test_PlcTag_Any(S7CommPlusClient conn, int nrandom) { int result = 0; Console.WriteLine("*** Test_PlcTag_Any ***"); @@ -1325,14 +1351,14 @@ private int Test_PlcTag_Any(S7CommPlusConnection conn, int nrandom) } tags.Add(tag); - conn.ReadTags(tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1} {2}", tag.Name, GetHexstring(tag.Value), tag.ToString())); foreach (var writevalue in testvalues) { tag.Value = writevalue; Console.WriteLine(String.Format("Name= {0,-20}: Write value = {1}", tag.Name, GetHexstring(writevalue))); - conn.WriteTags(tags); - conn.ReadTags(tags); + WriteTags(conn, tags); + ReadTags(conn, tags); Console.WriteLine(String.Format("Name= {0,-20}: Read value = {1} {2}", tag.Name, GetHexstring(tag.Value), tag.ToString())); if (tag.Value.SequenceEqual(writevalue)) { diff --git a/src/S7CommPlusDriver.Tests/AlarmTextFormatterTests.cs b/src/S7CommPlusDriver.Tests/AlarmTextFormatterTests.cs new file mode 100644 index 0000000..3584bdf --- /dev/null +++ b/src/S7CommPlusDriver.Tests/AlarmTextFormatterTests.cs @@ -0,0 +1,191 @@ +using System; +using S7CommPlusDriver.Alarming; +using Xunit; + +namespace S7CommPlusDriver.Tests +{ + public class AlarmTextFormatterTests + { + [Fact] + public void FormatReplacesAllTiaTagDisplayTypes() + { + var values = new S7CommPlusAlarmAssociatedValues(); + values.SD_1 = CreateInt(123); + values.SD_2 = CreateUInt(0x1Au); + values.SD_3 = CreateReal(12.345); + values.SD_4 = CreateString("Motor A"); + + var formatted = S7CommPlusAlarmTextFormatter.Format( + "d=@1%d@ u=@2%u@ x=@2%04X@ b=@2%08b@ f=@3%.2f@ s=@4%s@", + values, + 1033); + + Assert.Equal("d=123 u=26 x=16#001A b=2#00011010 f=12.35 s=Motor A", formatted); + } + + [Fact] + public void FormatSupportsSystemDiagnosisWidthPrefixBeforePercent() + { + var values = new S7CommPlusAlarmAssociatedValues(); + values.SD_2 = CreateInt(31); + + var formatted = S7CommPlusAlarmTextFormatter.Format( + "Kanal @2W%d@", + values, + 1031); + + Assert.Equal("Kanal 31", formatted); + } + + [Fact] + public void FormatUsesPackedStandardAssociatedValuesForExplicitElementTypes() + { + var values = S7CommPlusAlarmAssociatedValues.FromValueBlob( + new ValueBlobArray(new[] + { + new ValueBlob(0, new byte[] { 1, 1, 0 }), + new ValueBlob(0, new byte[] { 1, 2, 3, 4, 5, 6 }), + new ValueBlob(0, Array.Empty()) + })); + + var formatted = S7CommPlusAlarmTextFormatter.Format( + "B3=@3B%d@ W2=@2W%d@ W3=@3W%d@", + values, + 1031); + + Assert.Equal("B3=3 W2=772 W3=1286", formatted); + } + + [Fact] + public void FormatLeavesUnsupportedTextListPlaceholdersUnchanged() + { + var values = new S7CommPlusAlarmAssociatedValues(); + values.SD_1 = CreateInt(5); + + var formatted = S7CommPlusAlarmTextFormatter.Format( + "State @1%t#AlarmStates@", + values, + 1033); + + Assert.Equal("State @1%t#AlarmStates@", formatted); + } + + [Fact] + public void FormatResolvesTextListPlaceholdersWhenResolverIsProvided() + { + var values = new S7CommPlusAlarmAssociatedValues(); + values.SD_1 = CreateInt(5); + + var formatted = S7CommPlusAlarmTextFormatter.Format( + "State @1%t#AlarmStates@", + values, + 1033, + (listName, value, languageId) => + listName == "AlarmStates" && value == 5 && languageId == 1033 + ? "Running" + : null); + + Assert.Equal("State Running", formatted); + } + + [Fact] + public void FormatRecursivelyFormatsResolvedTextListEntries() + { + var values = new S7CommPlusAlarmAssociatedValues(); + values.SD_1 = CreateInt(17); + values.SD_2 = CreateInt(32769); + var catalog = new S7CommPlusTextListCatalog( + new[] { 1033 }, + new[] + { + new S7CommPlusTextList( + 519, + 1033, + S7CommPlusTextListScope.LanguageSpecific, + new[] { new S7CommPlusTextListEntry(32769, 32769, "Status @1%d@") }) + }); + + var formatted = S7CommPlusAlarmTextFormatter.Format( + "Alarm @2%t#519K@", + values, + 1033, + catalog.ResolveText); + + Assert.Equal("Alarm Status 17", formatted); + } + + [Fact] + public void TextListCatalogFallsBackForSystemWListNames() + { + var catalog = new S7CommPlusTextListCatalog( + new[] { 1033 }, + new[] + { + new S7CommPlusTextList( + 6, + 1033, + S7CommPlusTextListScope.LanguageSpecific, + new[] { new S7CommPlusTextListEntry(2, 2, "Input channel") }) + }); + + Assert.True(catalog.TryResolve("7W", 2, 1033, out var text)); + Assert.Equal("Input channel", text); + } + + [Fact] + public void FormatLeavesOtherTemplateTokensUnchanged() + { + var values = new S7CommPlusAlarmAssociatedValues(); + values.SD_1 = CreateInt(5); + + var formatted = S7CommPlusAlarmTextFormatter.Format( + "Block @BlockName@ operand $$Motor.Speed$$ value @1%d@", + values, + 1033); + + Assert.Equal("Block @BlockName@ operand $$Motor.Speed$$ value 5", formatted); + } + + [Fact] + public void FormatUsesLanguageCultureForFloatingPointValues() + { + var values = new S7CommPlusAlarmAssociatedValues(); + values.SD_1 = CreateReal(12.5); + + var formatted = S7CommPlusAlarmTextFormatter.Format( + "Wert @1%.1f@", + values, + 1031); + + Assert.Equal("Wert 12,5", formatted); + } + + private static S7CommPlusAlarmAssociatedValue CreateInt(long value) + { + var associatedValue = new S7CommPlusAlarmAssociatedValue(Ids.TI_DINT); + associatedValue.SetInt(value); + return associatedValue; + } + + private static S7CommPlusAlarmAssociatedValue CreateUInt(long value) + { + var associatedValue = new S7CommPlusAlarmAssociatedValue(Ids.TI_UDINT); + associatedValue.SetInt(value); + return associatedValue; + } + + private static S7CommPlusAlarmAssociatedValue CreateReal(double value) + { + var associatedValue = new S7CommPlusAlarmAssociatedValue(Ids.TI_LREAL); + associatedValue.SetReal(value); + return associatedValue; + } + + private static S7CommPlusAlarmAssociatedValue CreateString(string value) + { + var associatedValue = new S7CommPlusAlarmAssociatedValue(Ids.TI_STRING); + associatedValue.SetString(value); + return associatedValue; + } + } +} diff --git a/src/S7CommPlusDriver.Tests/FakeS7CommPlusSession.cs b/src/S7CommPlusDriver.Tests/FakeS7CommPlusSession.cs new file mode 100644 index 0000000..0066e30 --- /dev/null +++ b/src/S7CommPlusDriver.Tests/FakeS7CommPlusSession.cs @@ -0,0 +1,330 @@ +using S7CommPlusDriver.ClientApi; +using S7CommPlusDriver.Alarming; +using S7CommPlusDriver.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace S7CommPlusDriver.Tests +{ + internal sealed class FakeS7CommPlusSession : IS7CommPlusSession + { + public bool IsConnected { get; private set; } + public int ConnectCount { get; private set; } + public int DisconnectCount { get; private set; } + public int ReadCount { get; private set; } + public int MaxConcurrentReads { get; private set; } + public int ActiveReads; + public int CpuOperatingStateWriteCount { get; private set; } + public int? LastCpuOperatingStateRequest { get; private set; } + public List CpuOperatingStateRequests { get; } = new List(); + public int TagSubscriptionCreateCount { get; private set; } + public int TagSubscriptionWaitCount { get; private set; } + public int TagSubscriptionDeleteCount { get; private set; } + public int AlarmSubscriptionCreateCount { get; private set; } + public int AlarmSubscriptionWaitCount { get; private set; } + public int AlarmSubscriptionDeleteCount { get; private set; } + public int TisWatchSubscriptionCreateCount { get; private set; } + public int TisWatchSubscriptionWaitCount { get; private set; } + public int TisWatchSubscriptionDeleteCount { get; private set; } + public int LastActiveAlarmsLanguageId { get; private set; } + public string LastErrorDetail { get; set; } = ""; + public List CreatedTagSubscriptionIds { get; } = new List(); + public List CreatedAlarmSubscriptionIds { get; } = new List(); + public List CreatedTisWatchSubscriptionIds { get; } = new List(); + public List WaitedTagSubscriptionIds { get; } = new List(); + public List WaitedAlarmSubscriptionIds { get; } = new List(); + public List WaitedTisWatchSubscriptionIds { get; } = new List(); + public List DeletedTagSubscriptionIds { get; } = new List(); + public List DeletedAlarmSubscriptionIds { get; } = new List(); + public List DeletedTisWatchSubscriptionIds { get; } = new List(); + private uint _nextSubscriptionObjectId = 1; + + public Func? ConnectHandler { get; set; } + public Func? DisconnectHandler { get; set; } + public Func? LegitimateHandler { get; set; } + public Func<(int Error, List Variables)>? BrowseVariablesHandler { get; set; } + public Func<(int Error, List Blocks)>? BrowseBlocksHandler { get; set; } + public Func<(int Error, string StructureXml)>? PlcStructureXmlHandler { get; set; } + public Func? GetBlockHandler { get; set; } + public Func<(int Error, List Alarms)>? ActiveAlarmsHandler { get; set; } + public Func? GetTagHandler { get; set; } + public Func<(int Error, S7CommPlusCpuInfo CpuInfo)>? CpuInfoHandler { get; set; } + public Func<(int Error, S7CommPlusCpuState CpuState)>? CpuStateHandler { get; set; } + public Func<(int Error, S7CommPlusCpuCycleTime CycleTime)>? CpuCycleTimeHandler { get; set; } + public Func<(int Error, S7CommPlusCpuMemoryUsage MemoryUsage)>? CpuMemoryUsageHandler { get; set; } + public Func? SetCpuOperatingStateHandler { get; set; } + public Func<(int Error, S7CommPlusCpuCultureInfo CultureInfo)>? CpuCultureInfoHandler { get; set; } + public Func, (int Error, S7CommPlusTextListCatalog TextLists)>? TextListsHandler { get; set; } + public Func<(int Error, S7CommPlusCommunicationResourceSnapshot Resources)>? CommunicationResourcesHandler { get; set; } + public Func, (int Error, List Values, List Errors)>? ReadHandler { get; set; } + public Func, List, (int Error, List Errors)>? WriteHandler { get; set; } + public Func, ushort, short, int>? CreateTagSubscriptionHandler { get; set; } + public Func Notifications)>? WaitForTagSubscriptionHandler { get; set; } + public Func Notifications)>? WaitForTagSubscriptionByIdHandler { get; set; } + public Func? DeleteTagSubscriptionHandler { get; set; } + public Func? CreateAlarmSubscriptionHandler { get; set; } + public Func Notifications)>? WaitForAlarmSubscriptionHandler { get; set; } + public Func Notifications)>? WaitForAlarmSubscriptionByIdHandler { get; set; } + public Func? DeleteAlarmSubscriptionHandler { get; set; } + public Func? CreateTisWatchSubscriptionHandler { get; set; } + public Func Notifications)>? WaitForTisWatchSubscriptionHandler { get; set; } + public Func Notifications)>? WaitForTisWatchSubscriptionByIdHandler { get; set; } + public Func? DeleteTisWatchSubscriptionHandler { get; set; } + public string LastTisWatchDiagnostic { get; set; } = ""; + + public int Connect(S7CommPlusClientOptions options) + { + ConnectCount++; + var error = ConnectHandler?.Invoke(options) ?? 0; + IsConnected = error == 0; + return error; + } + + public int Disconnect(int timeoutMilliseconds) + { + DisconnectCount++; + IsConnected = false; + return DisconnectHandler?.Invoke(timeoutMilliseconds) ?? 0; + } + + public int CloseTransport(int timeoutMilliseconds) + { + DisconnectCount++; + IsConnected = false; + return DisconnectHandler?.Invoke(timeoutMilliseconds) ?? 0; + } + + public int Legitimate(string password, string username) + { + return LegitimateHandler?.Invoke(password, username) ?? 0; + } + + public int BrowseVariables(out List variables) + { + var result = BrowseVariablesHandler?.Invoke() ?? (0, new List()); + variables = result.Variables; + return result.Error; + } + + public int BrowseBlocks(out List blocks) + { + var result = BrowseBlocksHandler?.Invoke() ?? (0, new List()); + blocks = result.Blocks; + return result.Error; + } + + public int GetPlcStructureXml(out S7CommPlusPlcStructureSnapshot plcStructure) + { + var result = PlcStructureXmlHandler?.Invoke() ?? (0, string.Empty); + plcStructure = PlcStructureXmlParser.CreateSnapshot(result.StructureXml); + return result.Error; + } + + public int GetBlockContent(uint relationId, out S7CommPlusClientBlockContent blockContent) + { + var result = GetBlockHandler?.Invoke(relationId) ?? (0, new S7CommPlusClientBlockContent(relationId, $"Block_{relationId}", S7CommPlusProgrammingLanguage.SCL, relationId & 0xffff, S7CommPlusBlockType.FC, "", new Dictionary(), "", Array.Empty(), "", "", Array.Empty(), Array.Empty())); + blockContent = result.Block; + return result.Error; + } + + public int GetActiveAlarms(out List alarmList, int languageId, Func? textListResolver) + { + LastActiveAlarmsLanguageId = languageId; + var result = ActiveAlarmsHandler?.Invoke() ?? (0, new List()); + alarmList = result.Alarms; + return result.Error; + } + + public PlcTag GetPlcTagBySymbol(string symbol) + { + return GetTagHandler?.Invoke(symbol) ?? PlcTags.TagFactory(symbol, new ItemAddress("8A0E0001.F"), Softdatatype.S7COMMP_SOFTDATATYPE_INT); + } + + public int GetCpuInfo(out S7CommPlusCpuInfo cpuInfo) + { + var result = CpuInfoHandler?.Invoke() ?? (0, new S7CommPlusCpuInfo { PlcName = "TestCpu" }); + cpuInfo = result.CpuInfo; + return result.Error; + } + + public int GetCpuState(out S7CommPlusCpuState cpuState) + { + var result = CpuStateHandler?.Invoke() ?? (0, new S7CommPlusCpuState(8, S7CommPlusCpuOperatingState.Run)); + cpuState = result.CpuState; + return result.Error; + } + + public int GetCpuCycleTime(out S7CommPlusCpuCycleTime cycleTime) + { + var result = CpuCycleTimeHandler?.Invoke() ?? (0, new S7CommPlusCpuCycleTime(0, 150, 50.007, 50.012, 50.654)); + cycleTime = result.CycleTime; + return result.Error; + } + + public int GetCpuMemoryUsage(out S7CommPlusCpuMemoryUsage memoryUsage) + { + var result = CpuMemoryUsageHandler?.Invoke() ?? (0, new S7CommPlusCpuMemoryUsage(new[] + { + new S7CommPlusCpuMemoryArea("load", "Load memory", 1000, 120), + new S7CommPlusCpuMemoryArea("work-code", "Work memory code", 2000, 400) + })); + memoryUsage = result.MemoryUsage; + return result.Error; + } + + public int SetCpuOperatingState(int operatingStateRequest) + { + CpuOperatingStateWriteCount++; + LastCpuOperatingStateRequest = operatingStateRequest; + CpuOperatingStateRequests.Add(operatingStateRequest); + return SetCpuOperatingStateHandler?.Invoke(operatingStateRequest) ?? 0; + } + + public int GetCpuCultureInfo(out S7CommPlusCpuCultureInfo cultureInfo) + { + var result = CpuCultureInfoHandler?.Invoke() ?? (0, new S7CommPlusCpuCultureInfo(new[] { 1033 })); + cultureInfo = result.CultureInfo; + return result.Error; + } + + public int GetTextLists(IEnumerable languageIds, out S7CommPlusTextListCatalog textLists) + { + var result = TextListsHandler?.Invoke(languageIds) ?? (0, S7CommPlusTextListCatalog.Empty); + textLists = result.TextLists; + return result.Error; + } + + public int GetCommunicationResources(out S7CommPlusCommunicationResourceSnapshot resources) + { + var result = CommunicationResourcesHandler?.Invoke() ?? (0, new S7CommPlusCommunicationResourceSnapshot()); + resources = result.Resources; + return result.Error; + } + + public int ReadValues(List addresslist, out List values, out List errors) + { + ReadCount++; + var active = Interlocked.Increment(ref ActiveReads); + MaxConcurrentReads = Math.Max(MaxConcurrentReads, active); + try + { + var result = ReadHandler?.Invoke(addresslist) + ?? (0, new List { new ValueInt(123) }, new List { 0 }); + values = result.Values.Cast().ToList(); + errors = result.Errors; + if (result.Error != 0) + { + IsConnected = false; + } + return result.Error; + } + finally + { + Interlocked.Decrement(ref ActiveReads); + } + } + + public int WriteValues(List addresslist, List values, out List errors) + { + var result = WriteHandler?.Invoke(addresslist, values) + ?? (0, new List(new ulong[addresslist.Count])); + errors = result.Errors; + return result.Error; + } + + public int CreateTagSubscription(List tags, ushort cycleTimeMilliseconds, short initialCreditLimit, out uint subscriptionObjectId) + { + subscriptionObjectId = 0; + TagSubscriptionCreateCount++; + var result = CreateTagSubscriptionHandler?.Invoke(tags, cycleTimeMilliseconds, initialCreditLimit) ?? 0; + if (result == 0) + { + subscriptionObjectId = _nextSubscriptionObjectId++; + CreatedTagSubscriptionIds.Add(subscriptionObjectId); + } + return result; + } + + public int WaitForTagSubscriptionNotifications(uint subscriptionObjectId, int timeoutMilliseconds, short creditLimitStep, out List notifications) + { + TagSubscriptionWaitCount++; + WaitedTagSubscriptionIds.Add(subscriptionObjectId); + var result = WaitForTagSubscriptionByIdHandler?.Invoke(subscriptionObjectId, timeoutMilliseconds, creditLimitStep) + ?? WaitForTagSubscriptionHandler?.Invoke(timeoutMilliseconds, creditLimitStep) + ?? (S7Consts.errCliJobTimeout, new List()); + notifications = result.Notifications; + return result.Error; + } + + public int DeleteTagSubscription(uint subscriptionObjectId) + { + TagSubscriptionDeleteCount++; + DeletedTagSubscriptionIds.Add(subscriptionObjectId); + return DeleteTagSubscriptionHandler?.Invoke() ?? 0; + } + + public int CreateAlarmSubscription(uint[] languageIds, short initialCreditLimit, out uint subscriptionObjectId) + { + subscriptionObjectId = 0; + AlarmSubscriptionCreateCount++; + var result = CreateAlarmSubscriptionHandler?.Invoke(languageIds, initialCreditLimit) ?? 0; + if (result == 0) + { + subscriptionObjectId = _nextSubscriptionObjectId++; + CreatedAlarmSubscriptionIds.Add(subscriptionObjectId); + } + return result; + } + + public int WaitForAlarmNotifications(uint subscriptionObjectId, int timeoutMilliseconds, short creditLimitStep, out List notifications) + { + AlarmSubscriptionWaitCount++; + WaitedAlarmSubscriptionIds.Add(subscriptionObjectId); + var result = WaitForAlarmSubscriptionByIdHandler?.Invoke(subscriptionObjectId, timeoutMilliseconds, creditLimitStep) + ?? WaitForAlarmSubscriptionHandler?.Invoke(timeoutMilliseconds, creditLimitStep) + ?? (S7Consts.errCliJobTimeout, new List()); + notifications = result.Notifications; + return result.Error; + } + + public int DeleteAlarmSubscription(uint subscriptionObjectId) + { + AlarmSubscriptionDeleteCount++; + DeletedAlarmSubscriptionIds.Add(subscriptionObjectId); + return DeleteAlarmSubscriptionHandler?.Invoke() ?? 0; + } + + public int CreateTisWatchSubscription(S7CommPlusTisWatchRequest request, out uint subscriptionObjectId) + { + subscriptionObjectId = 0; + TisWatchSubscriptionCreateCount++; + var result = CreateTisWatchSubscriptionHandler?.Invoke(request) ?? 0; + if (result == 0) + { + subscriptionObjectId = _nextSubscriptionObjectId++; + CreatedTisWatchSubscriptionIds.Add(subscriptionObjectId); + } + return result; + } + + public int WaitForTisWatchNotifications(uint subscriptionObjectId, int timeoutMilliseconds, out List notifications) + { + TisWatchSubscriptionWaitCount++; + WaitedTisWatchSubscriptionIds.Add(subscriptionObjectId); + var result = WaitForTisWatchSubscriptionByIdHandler?.Invoke(subscriptionObjectId, timeoutMilliseconds) + ?? WaitForTisWatchSubscriptionHandler?.Invoke(timeoutMilliseconds) + ?? (S7Consts.errCliJobTimeout, new List()); + notifications = result.Notifications; + return result.Error; + } + + public int DeleteTisWatchSubscription(uint subscriptionObjectId) + { + TisWatchSubscriptionDeleteCount++; + DeletedTisWatchSubscriptionIds.Add(subscriptionObjectId); + return DeleteTisWatchSubscriptionHandler?.Invoke() ?? 0; + } + } +} diff --git a/src/S7CommPlusDriver.Tests/FakeS7Transport.cs b/src/S7CommPlusDriver.Tests/FakeS7Transport.cs new file mode 100644 index 0000000..c7d433d --- /dev/null +++ b/src/S7CommPlusDriver.Tests/FakeS7Transport.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; + +namespace S7CommPlusDriver.Tests +{ + internal sealed class FakeS7Transport : IS7Transport + { + private readonly Queue _receiveChunks = new Queue(); + + public bool Connected { get; set; } + public int ConnectCount { get; private set; } + public int CloseCount { get; private set; } + public int ConnectError { get; set; } + public int SendError { get; set; } + public int EmptyReceiveDelayMilliseconds { get; set; } + public List Sent { get; } = new List(); + public (string Address, int Port, int ConnectTimeout, int ReceiveTimeout, int SendTimeout) LastConnect { get; private set; } + + public void EnqueueReceive(byte[] data) + { + _receiveChunks.Enqueue(data); + } + + public int Connect(string address, int port, int connectTimeoutMilliseconds, int receiveTimeoutMilliseconds, int sendTimeoutMilliseconds) + { + ConnectCount++; + LastConnect = (address, port, connectTimeoutMilliseconds, receiveTimeoutMilliseconds, sendTimeoutMilliseconds); + Connected = ConnectError == 0; + return ConnectError; + } + + public int Send(byte[] buffer) + { + return Send(buffer, buffer.Length); + } + + public int Send(byte[] buffer, int size) + { + if (!Connected) + { + return S7Consts.errTCPNotConnected; + } + if (SendError != 0) + { + return SendError; + } + var sent = new byte[size]; + Array.Copy(buffer, sent, size); + Sent.Add(sent); + return 0; + } + + public int Receive(byte[] buffer, int start, int size) + { + if (!Connected) + { + return S7Consts.errTCPNotConnected; + } + if (_receiveChunks.Count == 0) + { + if (EmptyReceiveDelayMilliseconds > 0) + { + System.Threading.Thread.Sleep(EmptyReceiveDelayMilliseconds); + } + Connected = false; + return S7Consts.errTCPConnectionReset; + } + var chunk = _receiveChunks.Dequeue(); + if (chunk.Length != size) + { + return S7Consts.errIsoInvalidDataSize; + } + Array.Copy(chunk, 0, buffer, start, size); + return 0; + } + + public int Close() + { + CloseCount++; + Connected = false; + return 0; + } + + public void Dispose() + { + Close(); + } + } +} diff --git a/src/S7CommPlusDriver.Tests/LegacyChallengeHandshakeTests.cs b/src/S7CommPlusDriver.Tests/LegacyChallengeHandshakeTests.cs new file mode 100644 index 0000000..018a5cb --- /dev/null +++ b/src/S7CommPlusDriver.Tests/LegacyChallengeHandshakeTests.cs @@ -0,0 +1,421 @@ +using HarpoS7.Utilities.Auth; +using S7CommPlusDriver.Internal; +using System; +using System.Collections.Generic; +using System.IO; +using Xunit; + +namespace S7CommPlusDriver.Tests +{ + public class LegacyChallengeHandshakeTests + { + [Fact] + public void TryParseReadsChallengeFromServerSessionRequestAttribute() + { + var challenge = new byte[] + { + 0xB4, 0x16, 0x81, 0xBB, 0x96, 0x66, 0xDE, 0x00, 0xCF, 0xAF, + 0xC2, 0x7B, 0x4D, 0xB2, 0x01, 0x76, 0x4B, 0xDE, 0xF8, 0x37 + }; + var response = CreateResponse("03:B07654AC9CAA4ACA", challenge); + + Assert.True(LegacyChallengeHandshake.TryParse(Array.Empty(), response, out var handshake)); + + Assert.Equal((uint)0x256, handshake.SessionId); + Assert.Equal("03:B07654AC9CAA4ACA", handshake.Fingerprint); + Assert.Equal(EPublicKeyFamily.PlcSim, handshake.KeyFamily); + Assert.Equal(LegacyOmsPublicKeyFamily.VPlc, handshake.OmsKeyFamily); + Assert.Equal(challenge, handshake.Challenge); + Assert.Equal(LegacyAuthenticationFrameKind.Auto, handshake.AuthenticationFrameKind); + } + + [Fact] + public void TryParseFindsChallengeByAttributeShapeInsteadOfFixedOffset() + { + var pdu = Convert.FromHexString( + "720100ED32000004CA0000000136B088D0808084D68011028780809F598780809F5AA100000120821F0100" + + "A3816900151330333A42303736353441433943414134414341" + + "A3822B00048280808002" + + "A3822D0015245365727665723A204F4D53505F31362E30302E30302E30335F30312E31352E30302E3031" + + "A3822F100214B41681BB9666DE00CFAFC27B4DB201764BDEF837" + + "A3823200170000013A823B00048800823C00048701823D000484818640823E000484818540823F00151A313B364553372053494D2D30313530302D41504C433B53342E31" + + "824000150A323B313737323031343682410003000300A20000000072010000"); + var response = new CreateObjectResponse(1) + { + ObjectIds = new List { 0x256 }, + ResponseObject = new PObject() + }; + + Assert.True(LegacyChallengeHandshake.TryParse(pdu, response, out var handshake)); + + Assert.Equal(new byte[] + { + 0xB4, 0x16, 0x81, 0xBB, 0x96, 0x66, 0xDE, 0x00, 0xCF, 0xAF, + 0xC2, 0x7B, 0x4D, 0xB2, 0x01, 0x76, 0x4B, 0xDE, 0xF8, 0x37 + }, handshake.Challenge); + } + + [Fact] + public void TryParseSelectsV31CompactFrameForHighSessionS71500V3Response() + { + var pdu = Convert.FromHexString( + "720100ED32000004CA0000000136B088D0808084DA8011028780809C508780809C51A100000120821F0100" + + "A3816900151330303A31383142374230383437443131363934" + + "A3822B00048280808001" + + "A3822D0015245365727665723A204F4D53505F31342E30302E30305F36362E30372E30302E3031" + + "A3822F10021443D26063D1F0BFDB90BAA13BD5974A91CBBF7BDE" + + "A3823200170000013A823B00048701823C00048701823D000484818540823E000484818540823F00151A313B36455337203531352D32464E30332D304142303B56332E31" + + "824000150A323B313737323031343682410003000300A20000000072010000"); + var response = new CreateObjectResponse(1) + { + ObjectIds = new List { 0x70000E50 }, + ResponseObject = new PObject() + }; + + Assert.True(LegacyChallengeHandshake.TryParse(pdu, response, out var handshake)); + + Assert.Equal(EPublicKeyFamily.S71500, handshake.KeyFamily); + Assert.Equal(LegacyOmsPublicKeyFamily.Cpu1500, handshake.OmsKeyFamily); + Assert.Equal(LegacyAuthenticationFrameKind.S71500V31Compact, handshake.AuthenticationFrameKind); + } + + [Fact] + public void TryParseSelectsV31CompactFrameFromStructuredServerSessionVersion() + { + var challenge = new byte[] + { + 0x43, 0xD2, 0x60, 0x63, 0xD1, 0xF0, 0xBF, 0xDB, 0x90, 0xBA, + 0xA1, 0x3B, 0xD5, 0x97, 0x4A, 0x91, 0xCB, 0xBF, 0x7B, 0xDE + }; + var response = CreateResponse( + "00:181B7B0847D11694", + challenge, + objectId: 0x70000E50, + serverSessionVersion: "1;6ES7 515-2FN03-0AB0;V3.1"); + + Assert.True(LegacyChallengeHandshake.TryParse(Array.Empty(), response, out var handshake)); + + Assert.Equal("1;6ES7 515-2FN03-0AB0;V3.1", handshake.ServerSessionVersion); + Assert.Equal(LegacyAuthenticationFrameKind.S71500V31Compact, handshake.AuthenticationFrameKind); + } + + [Fact] + public void CreateAuthenticationRequestSupportsPlcSimFamily() + { + var publicKeyId = new byte[] { 0xCA, 0x4A, 0xAA, 0x9C, 0xAC, 0x54, 0x76, 0xB0 }; + var sessionKeyId = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF }; + var keyBlob = new byte[216]; + for (var i = 0; i < keyBlob.Length; i++) + { + keyBlob[i] = (byte)i; + } + + var request = LegacyChallengeHandshake.CreateAuthenticationRequest( + EPublicKeyFamily.PlcSim, + 0x70000FDB, + publicKeyId, + sessionKeyId, + keyBlob, + CreateSessionVersion("2;17720146"), + LegacyServerSessionRole.EngineeringSystem); + + Assert.NotNull(request); + Assert.Equal(ProtocolVersion.V2, request.ProtocolVersion); + Assert.Equal(2, request.SequenceNumber); + Assert.Equal((uint)0x70000FDB, request.SessionId); + Assert.Equal((uint)0x70000FDB, request.InObjectId); + Assert.Equal(new uint[] { Ids.SessionKey, Ids.ServerSessionVersion, Ids.ServerSessionRole, Ids.LegacyAuthenticationCompatibilityFlag }, request.AddressList); + + var payload = Serialize(request); + Assert.True(ContainsSequence(payload, Convert.FromHexString("8E220005D89DCACAE4F2D4CACA"))); + Assert.True(ContainsSequence(payload, Convert.FromHexString("8E2300048610"))); + Assert.True(ContainsSequence(payload, Convert.FromHexString("8E220005F7F3B5B8CB9D8AA301"))); + Assert.True(ContainsSequence(payload, Convert.FromHexString("8E2300048601"))); + Assert.True(ContainsSequence(payload, Convert.FromHexString("8E0D0014008158"))); + Assert.True(ContainsSequence(payload, keyBlob)); + Assert.True(ContainsSequence(payload, Convert.FromHexString("03000401"))); + } + + [Fact] + public void CreateObjectRequestCanUseTiaServerSessionShape() + { + var request = new CreateObjectRequest(ProtocolVersion.V1, 1, false) + { + SessionId = Ids.ObjectNullServerSession + }; + request.SetTiaServerSessionData(LegacyServerSessionRole.EngineeringSystem); + var hmiRequest = new CreateObjectRequest(ProtocolVersion.V1, 1, false) + { + SessionId = Ids.ObjectNullServerSession + }; + hmiRequest.SetTiaServerSessionData(LegacyServerSessionRole.Hmi); + using var stream = new MemoryStream(); + using var hmiStream = new MemoryStream(); + + request.Serialize(stream); + hmiRequest.Serialize(hmiStream); + + var payload = stream.ToArray(); + var hmiPayload = hmiStream.ToArray(); + Assert.Equal(Opcode.Request, payload[0]); + Assert.Equal(0x04CA, (payload[3] << 8) | payload[4]); + Assert.True(ContainsSequence(payload, Convert.FromHexString("A1000000D3821F0000"))); + Assert.True(ContainsSequence(payload, Convert.FromHexString("A381690015"))); + Assert.True(ContainsSequence(payload, System.Text.Encoding.ASCII.GetBytes("S7CommPlusDriver.TCPIP.1"))); + Assert.True(ContainsSequence(payload, Convert.FromHexString("A3822B0004" + ((byte)LegacyServerSessionRole.EngineeringSystem).ToString("X2")))); + Assert.True(ContainsSequence(hmiPayload, Convert.FromHexString("A3822B0004" + ((byte)LegacyServerSessionRole.Hmi).ToString("X2")))); + Assert.True(ContainsSequence(payload, Convert.FromHexString("A3822C0012"))); + Assert.True(ContainsSequence(payload, System.Text.Encoding.ASCII.GetBytes("SubscriptionContainer"))); + } + + [Fact] + public void SiemensOmsConstantsCaptureLegacySecurityVocabulary() + { + Assert.Equal(2, (int)LegacyOmsSecurityType.Csi); + Assert.Equal(4, (int)LegacyOmsSecurityType.Tls); + Assert.Equal(299, LegacyOmsConstants.ServerSessionRole); + Assert.Equal(303, LegacyOmsConstants.ServerSessionChallenge); + Assert.Equal(LegacyOmsConstants.ServerSessionChallenge, LegacyOmsConstants.SessionServerChallenge); + Assert.Equal(304, LegacyOmsConstants.ServerSessionResponse); + Assert.Equal(305, LegacyOmsConstants.ServerSessionRoles); + Assert.Equal(306, LegacyOmsConstants.ServerSessionClientVersion); + Assert.Equal(309, LegacyOmsConstants.ClientSessionPassword); + Assert.Equal(310, LegacyOmsConstants.ClientSessionLegitimated); + Assert.Equal(311, LegacyOmsConstants.ClientSessionCommunicationFormat); + Assert.Equal(314, LegacyOmsConstants.LidSessionVersionStruct); + Assert.Equal(315, LegacyOmsConstants.LidSessionVersionSystemOms); + Assert.Equal(316, LegacyOmsConstants.LidSessionVersionProjectOms); + Assert.Equal(317, LegacyOmsConstants.LidSessionVersionSystemPaom); + Assert.Equal(318, LegacyOmsConstants.LidSessionVersionProjectPaom); + Assert.Equal(319, LegacyOmsConstants.LidSessionVersionSystemPaomString); + Assert.Equal(320, LegacyOmsConstants.LidSessionVersionProjectPaomString); + Assert.Equal(321, LegacyOmsConstants.LidSessionVersionProjectFormat); + Assert.Equal(1800, LegacyOmsConstants.StructSecurityKey); + Assert.Equal(1801, LegacyOmsConstants.SecurityKeyVersion); + Assert.Equal(1802, LegacyOmsConstants.SecurityKeySecurityLevel); + Assert.Equal(1803, LegacyOmsConstants.SecurityKeyPublicKeyId); + Assert.Equal(1804, LegacyOmsConstants.SecurityKeySymmetricKeyId); + Assert.Equal(1805, LegacyOmsConstants.SecurityKeyEncryptedKey); + Assert.Equal(1811, LegacyOmsConstants.EncryptionData); + Assert.Equal(1820, LegacyOmsConstants.StructMac); + Assert.Equal(1821, LegacyOmsConstants.MacAlgorithm); + Assert.Equal(1822, LegacyOmsConstants.MacEncryptedKey); + Assert.Equal(1823, LegacyOmsConstants.MacData); + Assert.Equal(1825, LegacyOmsConstants.SecurityKeyId); + Assert.Equal(1826, LegacyOmsConstants.SecurityKeyIdValue); + Assert.Equal(1827, LegacyOmsConstants.SecurityKeyIdFlags); + Assert.Equal(1828, LegacyOmsConstants.SecurityKeyIdInternalFlags); + Assert.Equal(1830, LegacyOmsConstants.ServerSessionSessionKey); + Assert.Equal(LegacyOmsConstants.ServerSessionSessionKey, LegacyOmsConstants.SessionKey); + Assert.Equal(1842, LegacyOmsConstants.EffectiveProtectionLevel); + Assert.Equal(LegacyOmsConstants.EffectiveProtectionLevel, LegacyOmsConstants.CurrentPlcSecurityLevel); + Assert.Equal(1843, LegacyOmsConstants.ActiveProtectionLevel); + Assert.Equal(1844, LegacyOmsConstants.ExpectedLegitimationLevel); + Assert.Equal(1845, LegacyOmsConstants.CollaborationToken); + Assert.Equal(1846, LegacyOmsConstants.Legitimate); + Assert.Equal(LegacyOmsConstants.Legitimate, LegacyOmsConstants.LegacySessionSecretResponse); + Assert.Equal(1902, LegacyOmsConstants.LegacyAuthenticationCompatibilityFlag); + Assert.Equal(LegacyOmsConstants.SessionServerChallenge, Ids.SessionServerChallenge); + Assert.Equal(LegacyOmsConstants.SecurityKeyPublicKeyId, Ids.SecurityKeyPublicKeyID); + Assert.Equal(LegacyOmsConstants.SecurityKeyPublicKeyId, Ids.SecurityKeyPublicKeyId); + Assert.Equal(LegacyOmsConstants.SecurityKeySymmetricKeyId, Ids.SecurityKeySymmetricKeyID); + Assert.Equal(LegacyOmsConstants.SecurityKeySymmetricKeyId, Ids.SecurityKeySymmetricKeyId); + Assert.Equal(LegacyOmsConstants.SecurityKeyEncryptedKey, Ids.SecurityKeyEncryptedKey); + Assert.Equal(LegacyOmsConstants.MacData, Ids.MACData); + Assert.Equal(LegacyOmsConstants.SecurityKeyId, Ids.SecurityKeyID); + Assert.Equal(LegacyOmsConstants.SecurityKeyId, Ids.SecurityKeyId); + Assert.Equal(LegacyOmsConstants.SecurityKeyIdValue, Ids.SecurityKeyIdValue); + Assert.Equal(LegacyOmsConstants.SecurityKeyIdFlags, Ids.SecurityKeyIdFlags); + Assert.Equal(LegacyOmsConstants.SecurityKeyIdInternalFlags, Ids.SecurityKeyIdInternalFlags); + Assert.Equal(LegacyOmsConstants.SessionKey, Ids.SessionKey); + Assert.Equal(LegacyOmsConstants.CurrentPlcSecurityLevel, Ids.CurrentPlcSecurityLevel); + Assert.Equal(LegacyOmsConstants.ExpectedLegitimationLevel, Ids.ExpectedLegitimationLevel); + Assert.Equal(LegacyOmsConstants.LegacySessionSecretResponse, Ids.LegacySessionSecretResponse); + Assert.Equal(LegacyOmsConstants.LidSessionVersionStruct, Ids.LID_SessionVersionStruct); + Assert.Equal(LegacyOmsConstants.LidSessionVersionSystemOms, Ids.LID_SessionVersionSystemOMS); + Assert.Equal(LegacyOmsConstants.LidSessionVersionProjectOms, Ids.LID_SessionVersionProjectOMS); + Assert.Equal(LegacyOmsConstants.LidSessionVersionSystemPaom, Ids.LID_SessionVersionSystemPAOM); + Assert.Equal(LegacyOmsConstants.LidSessionVersionProjectPaom, Ids.LID_SessionVersionProjectPAOM); + Assert.Equal(LegacyOmsConstants.LidSessionVersionSystemPaomString, Ids.LID_SessionVersionSystemPAOMString); + Assert.Equal(LegacyOmsConstants.LidSessionVersionProjectPaomString, Ids.LID_SessionVersionProjectPAOMString); + Assert.Equal(LegacyOmsConstants.LidSessionVersionProjectFormat, Ids.LID_SessionVersionProjectFormat); + Assert.Equal(LegacyOmsConstants.LegacyAuthenticationCompatibilityFlag, Ids.LegacyAuthenticationCompatibilityFlag); + Assert.Equal(0x0000, (int)LegacyOmsPublicKeyFamily.Cpu1500); + Assert.Equal(0x0100, (int)LegacyOmsPublicKeyFamily.Cpu1200); + Assert.Equal(0x0300, (int)LegacyOmsPublicKeyFamily.VPlc); + } + + [Fact] + public void HarpoKeyFlagsUseSiemensCsiLayout() + { + Assert.Equal(0x01, SiemensCsiKeyFlags.GetSymmetricKeyFlags(EPublicKeyFamily.S71500)); + Assert.Equal(0x101, SiemensCsiKeyFlags.GetSymmetricKeyFlags(EPublicKeyFamily.S71200)); + Assert.Equal(0x301, SiemensCsiKeyFlags.GetSymmetricKeyFlags(EPublicKeyFamily.PlcSim)); + Assert.Equal(0x10, SiemensCsiKeyFlags.GetCommPublicKeyFlags(EPublicKeyFamily.S71500)); + Assert.Equal(0x110, SiemensCsiKeyFlags.GetCommPublicKeyFlags(EPublicKeyFamily.S71200)); + Assert.Equal(0x310, SiemensCsiKeyFlags.GetCommPublicKeyFlags(EPublicKeyFamily.PlcSim)); + Assert.Equal("KeyFamilyCPU1500", SiemensCsiKeyFlags.GetSiemensFamilyName(EPublicKeyFamily.S71500)); + } + + [Fact] + public void CreateAuthenticationRequestForS71500HighSessionUsesRolesWriteShape() + { + var publicKeyId = Convert.FromHexString("9416D147087B1B18"); + var sessionKeyId = Convert.FromHexString("4E0C313B5E08E43B"); + var keyBlob = new byte[180]; + for (var i = 0; i < keyBlob.Length; i++) + { + keyBlob[i] = (byte)i; + } + + var request = LegacyChallengeHandshake.CreateAuthenticationRequest( + EPublicKeyFamily.S71500, + 0x70000CC5, + publicKeyId, + sessionKeyId, + keyBlob, + CreateSessionVersion("2;353025"), + LegacyServerSessionRole.EngineeringSystem); + + Assert.NotNull(request); + Assert.Equal(new uint[] { Ids.SessionKey, Ids.ServerSessionVersion, Ids.ServerSessionRoles }, request.AddressList); + var payload = Serialize(request); + Assert.True(ContainsSequence(payload, Convert.FromHexString("8E2200058C86EFB0C29FA29694"))); + Assert.True(ContainsSequence(payload, Convert.FromHexString("8E23000410"))); + Assert.True(ContainsSequence(payload, Convert.FromHexString("8E2200059DF98185F1ECE28C4E"))); + Assert.True(ContainsSequence(payload, Convert.FromHexString("8E23000401"))); + Assert.True(ContainsSequence(payload, keyBlob)); + Assert.True(ContainsSequence(payload, Convert.FromHexString("032003010002"))); + } + + [Fact] + public void CreateAuthenticationRequestForLowSessionS71500UsesTwoItemShape() + { + var publicKeyId = Convert.FromHexString("9416D147087B1B18"); + var sessionKeyId = Convert.FromHexString("4E0C313B5E08E43B"); + var keyBlob = new byte[180]; + for (var i = 0; i < keyBlob.Length; i++) + { + keyBlob[i] = (byte)i; + } + + var request = LegacyChallengeHandshake.CreateAuthenticationRequest( + EPublicKeyFamily.S71500, + 0x000003DF, + publicKeyId, + sessionKeyId, + keyBlob, + CreateSessionVersion("2;17253152"), + LegacyServerSessionRole.EngineeringSystem); + + Assert.NotNull(request); + Assert.Equal(new uint[] { Ids.SessionKey, Ids.ServerSessionVersion }, request.AddressList); + var payload = Serialize(request); + Assert.True(ContainsSequence(payload, Convert.FromHexString("8E2200058C86EFB0C29FA29694"))); + Assert.True(ContainsSequence(payload, Convert.FromHexString("8E2200059DF98185F1ECE28C4E"))); + Assert.True(ContainsSequence(payload, keyBlob)); + } + + [Fact] + public void CreateAuthenticationRequestForV31S71500UsesRoleItemShape() + { + var publicKeyId = Convert.FromHexString("9416D147087B1B18"); + var sessionKeyId = Convert.FromHexString("4E0C313B5E08E43B"); + var keyBlob = new byte[180]; + for (var i = 0; i < keyBlob.Length; i++) + { + keyBlob[i] = (byte)i; + } + + var request = LegacyChallengeHandshake.CreateAuthenticationRequest( + EPublicKeyFamily.S71500, + 0x70000E50, + publicKeyId, + sessionKeyId, + keyBlob, + CreateSessionVersion("2;17720146"), + LegacyServerSessionRole.EngineeringSystem, + LegacyAuthenticationFrameKind.S71500V31Compact); + + Assert.NotNull(request); + Assert.Equal(new uint[] { Ids.SessionKey, Ids.ServerSessionVersion, Ids.ServerSessionRole }, request.AddressList); + var payload = Serialize(request); + Assert.True(ContainsSequence(payload, Convert.FromHexString("8E2200058C86EFB0C29FA29694"))); + Assert.True(ContainsSequence(payload, Convert.FromHexString("8E2200059DF98185F1ECE28C4E"))); + Assert.True(ContainsSequence(payload, keyBlob)); + Assert.True(ContainsSequence(payload, Convert.FromHexString("03000401"))); + } + + [Fact] + public void GetVarSubstreamedRequestUsesZeroUnknownFieldBeforeIntegrityId() + { + var request = new GetVarSubstreamedRequest(ProtocolVersion.V3) + { + SequenceNumber = 3, + SessionId = 0x70000CC5, + IntegrityId = 0, + InObjectId = 0x70000CC5, + Address = Ids.EffectiveProtectionLevel + }; + using var buffer = new MemoryStream(); + + request.Serialize(buffer); + + var payload = buffer.ToArray(); + Assert.Equal( + Convert.FromHexString("31000005860000000370000CC53470000CC52004018E32000004E88969001200000000896A001300896B000400000001000000000000"), + payload); + } + + private static CreateObjectResponse CreateResponse(string fingerprint, byte[] challenge, uint objectId = 0x256, string serverSessionVersion = "") + { + var response = new CreateObjectResponse(1) + { + ObjectIds = new List { objectId }, + ResponseObject = new PObject() + }; + response.ResponseObject.AddAttribute(Ids.ObjectVariableTypeName, new ValueWString(fingerprint)); + response.ResponseObject.AddAttribute(Ids.ServerSessionRequest, new ValueUSIntArray(challenge)); + if (!string.IsNullOrEmpty(serverSessionVersion)) + { + var versionStruct = new ValueStruct(0); + versionStruct.AddStructElement(Ids.LID_SessionVersionSystemPAOMString, new ValueWString(serverSessionVersion)); + response.ResponseObject.AddAttribute(Ids.ServerSessionVersion, versionStruct); + } + return response; + } + + private static ValueStruct CreateSessionVersion(string serverSessionVersion) + { + var versionStruct = new ValueStruct(Ids.LID_SessionVersionStruct); + versionStruct.AddStructElement(Ids.LID_SessionVersionSystemOMS, new ValueUDInt(1024)); + versionStruct.AddStructElement(Ids.LID_SessionVersionProjectOMS, new ValueUDInt(897)); + versionStruct.AddStructElement(Ids.LID_SessionVersionSystemPAOM, new ValueUDInt(8405696)); + versionStruct.AddStructElement(Ids.LID_SessionVersionProjectPAOM, new ValueUDInt(8405696)); + versionStruct.AddStructElement(Ids.LID_SessionVersionSystemPAOMString, new ValueWString(string.Empty)); + versionStruct.AddStructElement(Ids.LID_SessionVersionProjectPAOMString, new ValueWString(serverSessionVersion)); + versionStruct.AddStructElement(Ids.LID_SessionVersionProjectFormat, new ValueUInt(3)); + return versionStruct; + } + + private static byte[] Serialize(SetMultiVariablesRequest request) + { + using var stream = new MemoryStream(); + request.Serialize(stream); + return stream.ToArray(); + } + + private static bool ContainsSequence(byte[] haystack, byte[] needle) + { + for (var i = 0; i <= haystack.Length - needle.Length; i++) + { + if (haystack.AsSpan(i, needle.Length).SequenceEqual(needle)) + { + return true; + } + } + + return false; + } + } +} diff --git a/src/S7CommPlusDriver.Tests/LiveReadOnlySmokeTests.cs b/src/S7CommPlusDriver.Tests/LiveReadOnlySmokeTests.cs new file mode 100644 index 0000000..d065266 --- /dev/null +++ b/src/S7CommPlusDriver.Tests/LiveReadOnlySmokeTests.cs @@ -0,0 +1,81 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace S7CommPlusDriver.Tests +{ + public sealed class LiveReadOnlySmokeTests + { + [Fact] + public async Task LivePlcReadOnlySmokeTest() + { + var host = Environment.GetEnvironmentVariable("S7COMMPLUS_LIVE_HOST"); + if (string.IsNullOrWhiteSpace(host)) + { + return; + } + + var securityModeName = Environment.GetEnvironmentVariable("S7COMMPLUS_LIVE_SECURITY_MODE"); + var securityMode = S7CommPlusSecurityMode.Tls; + if (!string.IsNullOrWhiteSpace(securityModeName)) + { + Assert.True(Enum.TryParse(securityModeName, ignoreCase: true, out securityMode), $"Invalid S7COMMPLUS_LIVE_SECURITY_MODE value '{securityModeName}'."); + } + + var tlsBackendName = Environment.GetEnvironmentVariable("S7COMMPLUS_LIVE_TLS_BACKEND"); + var tlsBackend = new S7CommPlusClientOptions().TlsBackend; + if (!string.IsNullOrWhiteSpace(tlsBackendName)) + { + Assert.True(Enum.TryParse(tlsBackendName, ignoreCase: true, out tlsBackend), $"Invalid S7COMMPLUS_LIVE_TLS_BACKEND value '{tlsBackendName}'."); + } + + await using var client = new S7CommPlusClient(new S7CommPlusClientOptions + { + Address = host, + SecurityMode = securityMode, + TlsBackend = tlsBackend, + RequestTimeout = ReadOptionalTimeout("S7COMMPLUS_LIVE_REQUEST_TIMEOUT_SECONDS", TimeSpan.FromSeconds(5)), + ConnectTimeout = ReadOptionalTimeout("S7COMMPLUS_LIVE_CONNECT_TIMEOUT_SECONDS", TimeSpan.FromSeconds(5)) + }); + await client.ConnectAsync(); + var cpuInfo = await client.GetCpuInfoAsync(); + var cultureInfo = await client.GetCpuCultureInfoAsync(); + var textLists = await client.GetTextListsAsync(); + var vars = await client.BrowseAsync(); + + Assert.NotNull(cpuInfo); + Assert.NotNull(cultureInfo); + Assert.NotNull(cultureInfo.LanguageIds); + Assert.NotEmpty(cultureInfo.LanguageIds); + Assert.NotNull(textLists); + Assert.NotEmpty(textLists.TextLists); + Assert.NotNull(vars); + + var tagNames = Environment.GetEnvironmentVariable("S7COMMPLUS_LIVE_TAGS"); + if (!string.IsNullOrWhiteSpace(tagNames)) + { + var requested = tagNames.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var tagTasks = requested.Select(symbol => client.GetTagBySymbolAsync(symbol)).ToArray(); + var tags = await Task.WhenAll(tagTasks); + var readResult = await client.ReadAsync(tags); + Assert.NotEmpty(tags); + Assert.All(readResult.Items, item => Assert.True(item.IsSuccess, $"Tag {item.Tag.Name} read failed with item error {item.ItemError}.")); + } + + await client.DisconnectAsync(); + } + + private static TimeSpan ReadOptionalTimeout(string environmentVariable, TimeSpan fallback) + { + var value = Environment.GetEnvironmentVariable(environmentVariable); + if (string.IsNullOrWhiteSpace(value)) + { + return fallback; + } + + Assert.True(double.TryParse(value, out var seconds) && seconds > 0, $"Invalid {environmentVariable} value '{value}'."); + return TimeSpan.FromSeconds(seconds); + } + } +} diff --git a/src/S7CommPlusDriver.Tests/MsgSocketTests.cs b/src/S7CommPlusDriver.Tests/MsgSocketTests.cs new file mode 100644 index 0000000..43a6311 --- /dev/null +++ b/src/S7CommPlusDriver.Tests/MsgSocketTests.cs @@ -0,0 +1,39 @@ +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using Xunit; + +namespace S7CommPlusDriver.Tests +{ + public sealed class MsgSocketTests + { + [Fact] + public async Task ReceiveTimeoutKeepsConnectionOpen() + { + using var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + + var acceptTask = listener.AcceptSocketAsync(); + var socket = new MsgSocket + { + ConnectTimeout = 1000, + ReadTimeout = 100, + WriteTimeout = 1000 + }; + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + + var connectError = socket.Connect(IPAddress.Loopback.ToString(), port); + using var serverSocket = await acceptTask; + + var buffer = new byte[1]; + var receiveError = socket.Receive(buffer, 0, buffer.Length); + var connectedAfterTimeout = socket.Connected; + + socket.Close(); + + Assert.Equal(0, connectError); + Assert.Equal(S7Consts.errTCPReceiveTimeout, receiveError); + Assert.True(connectedAfterTimeout); + } + } +} diff --git a/src/S7CommPlusDriver.Tests/S7ClientTransportTests.cs b/src/S7CommPlusDriver.Tests/S7ClientTransportTests.cs new file mode 100644 index 0000000..b0e41dd --- /dev/null +++ b/src/S7CommPlusDriver.Tests/S7ClientTransportTests.cs @@ -0,0 +1,59 @@ +using Xunit; + +namespace S7CommPlusDriver.Tests +{ + public sealed class S7ClientTransportTests + { + [Fact] + public void ConnectUsesInjectedTransportAndTimeouts() + { + var transport = new FakeS7Transport { ConnectError = S7Consts.errTCPConnectionFailed }; + var client = new S7Client(() => transport) + { + PLCPort = 120, + ConnTimeout = 111, + RecvTimeout = 222, + SendTimeout = 333 + }; + client.SetConnectionParams("1.2.3.4", 0x0600, new byte[] { 1, 2, 3 }); + + var error = client.Connect(); + + Assert.Equal(S7Consts.errTCPConnectionFailed, error); + Assert.Equal(1, transport.ConnectCount); + Assert.Equal(("1.2.3.4", 120, 111, 222, 333), transport.LastConnect); + } + + [Fact] + public void ConnectAcceptsConnectionConfirmForShortRemoteTsap() + { + var transport = new FakeS7Transport { EmptyReceiveDelayMilliseconds = 500 }; + transport.EnqueueReceive(new byte[] { 0x03, 0x00, 0x00, 0x23 }); + transport.EnqueueReceive(new byte[] { 0x1E, 0xD0, 0x00 }); + transport.EnqueueReceive(new byte[28]); + + var client = new S7Client(() => transport); + client.SetConnectionParams("1.2.3.4", 0x0600, System.Text.Encoding.ASCII.GetBytes(S7CommPlusDefaults.RemoteTsapEs)); + + var error = client.Connect(); + client.Disconnect(50); + + Assert.Equal(0, error); + Assert.Single(transport.Sent); + Assert.Equal(35, transport.Sent[0].Length); + } + + [Fact] + public void DisconnectClosesInjectedTransport() + { + var transport = new FakeS7Transport { Connected = true }; + var client = new S7Client(() => transport); + + var error = client.Disconnect(50); + + Assert.Equal(0, error); + Assert.Equal(1, transport.CloseCount); + Assert.False(transport.Connected); + } + } +} diff --git a/src/S7CommPlusDriver.Tests/S7CommPlusClientTests.cs b/src/S7CommPlusDriver.Tests/S7CommPlusClientTests.cs new file mode 100644 index 0000000..1cde9f3 --- /dev/null +++ b/src/S7CommPlusDriver.Tests/S7CommPlusClientTests.cs @@ -0,0 +1,1270 @@ +using S7CommPlusDriver.ClientApi; +using S7CommPlusDriver.Alarming; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace S7CommPlusDriver.Tests +{ + public sealed class S7CommPlusClientTests + { + [Fact] + public async Task ConnectBrowseAndReadSucceeds() + { + var fake = new FakeS7CommPlusSession + { + BrowseVariablesHandler = () => (0, new List { new VarInfo { Name = "DB.Value" } }) + }; + var client = CreateClient(fake); + + await client.ConnectAsync(); + var vars = await client.BrowseAsync(); + var read = await client.ReadAsync(new[] { new ItemAddress("8A0E0001.F") }); + + Assert.Equal(S7CommPlusConnectionState.Connected, client.State); + Assert.Single(vars); + Assert.Single(read.Items); + Assert.True(read.Items[0].IsSuccess); + } + + [Fact] + public async Task DisconnectIsIdempotent() + { + var fake = new FakeS7CommPlusSession(); + var client = CreateClient(fake); + + await client.DisconnectAsync(); + await client.ConnectAsync(); + await client.DisconnectAsync(); + await client.DisconnectAsync(); + + Assert.Equal(S7CommPlusConnectionState.Disconnected, client.State); + Assert.Equal(1, fake.DisconnectCount); + } + + [Fact] + public async Task RequestTimeoutIsTypedFailure() + { + var fake = new FakeS7CommPlusSession + { + BrowseVariablesHandler = () => + { + Task.Delay(200).Wait(); + return (0, new List()); + } + }; + var client = CreateClient(fake, requestTimeoutMs: 20); + + var ex = await Assert.ThrowsAsync(() => client.BrowseAsync()); + + Assert.Equal("Browse", ex.Operation); + Assert.True(ex.IsTransient); + } + + [Fact] + public async Task ReadReconnectsOnceAfterTransientDisconnect() + { + var fake = new FakeS7CommPlusSession(); + fake.ReadHandler = _ => + { + if (fake.ReadCount == 1) + { + return (S7Consts.errTCPDataReceive, new List(), new List()); + } + return (0, new List { new ValueInt(7) }, new List { 0 }); + }; + var client = CreateClient(fake); + + var result = await client.ReadAsync(new[] { new ItemAddress("8A0E0001.F") }); + + Assert.Equal(2, fake.ConnectCount); + Assert.Equal(2, fake.ReadCount); + Assert.True(result.Items[0].IsSuccess); + } + + [Fact] + public async Task ProtocolErrorDoesNotReconnect() + { + var fake = new FakeS7CommPlusSession + { + ReadHandler = _ => (S7Consts.errIsoInvalidPDU3, new List(), new List()) + }; + var client = CreateClient(fake); + + var ex = await Assert.ThrowsAsync(() => client.ReadAsync(new[] { new ItemAddress("8A0E0001.F") })); + + Assert.Equal(S7Consts.errIsoInvalidPDU3, ex.ErrorCode); + Assert.False(ex.IsTransient); + Assert.Equal(1, fake.ConnectCount); + } + + [Fact] + public async Task PartialBatchReadReturnsPerItemErrors() + { + var fake = new FakeS7CommPlusSession + { + ReadHandler = addresses => (0, + new List { new ValueInt(1), null }, + new List { 0, 0xDEAD }) + }; + var client = CreateClient(fake); + + var result = await client.ReadAsync(new[] { new ItemAddress("8A0E0001.F"), new ItemAddress("8A0E0001.10") }); + + Assert.True(result.Items[0].IsSuccess); + Assert.False(result.Items[1].IsSuccess); + Assert.Equal((ulong)0xDEAD, result.Items[1].ItemError); + } + + [Fact] + public async Task EmptyReadsReturnEmptyResultsWithoutConnecting() + { + var fake = new FakeS7CommPlusSession(); + var client = CreateClient(fake); + + var addressRead = await client.ReadAsync(Array.Empty()); + var tagRead = await client.ReadAsync(Array.Empty()); + + Assert.Empty(addressRead.Items); + Assert.Empty(tagRead.Items); + Assert.Equal(0, fake.ConnectCount); + } + + [Fact] + public async Task EmptyWritesReturnEmptyResultsWithoutConnecting() + { + var fake = new FakeS7CommPlusSession(); + var client = CreateClient(fake); + + var addressWrite = await client.WriteAsync(Array.Empty(), Array.Empty()); + var tagWrite = await client.WriteAsync(Array.Empty()); + + Assert.Empty(addressWrite.Items); + Assert.Empty(tagWrite.Items); + Assert.Equal(0, fake.ConnectCount); + } + + [Fact] + public void ItemAddressRejectsMalformedAccessStrings() + { + Assert.Throws(() => new ItemAddress("")); + Assert.Throws(() => new ItemAddress("8A0E0001")); + Assert.Throws(() => new ItemAddress("8A0E0001.NOTHEX")); + } + + [Fact] + public void PlcTagCharRejectsCharactersThatDoNotFitOnePlcByte() + { + var tag = new PlcTagChar("CharValue", new ItemAddress("8A0E0001.F"), Softdatatype.S7COMMP_SOFTDATATYPE_CHAR); + + tag.Value = 'A'; + + Assert.Equal('A', tag.Value); + Assert.Throws(() => tag.Value = '\u20AC'); + } + + [Fact] + public void VlqDecodersRejectTruncatedValues() + { + Assert.Throws(() => S7p.DecodeUInt32Vlq(new MemoryStream(new byte[] { 0x80 }), out _)); + Assert.Throws(() => S7p.DecodeUInt64Vlq(new MemoryStream(new byte[] { 0x80 }), out _)); + Assert.Throws(() => S7p.DecodeInt32Vlq(new MemoryStream(new byte[] { 0x80 }), out _)); + Assert.Throws(() => S7p.DecodeInt64Vlq(new MemoryStream(new byte[] { 0x80 }), out _)); + } + + [Fact] + public void BlobDecompressorReturnsPayloadWhenOnlyZlibTrailerIsMissing() + { + const string xml = ""; + using var compressed = new MemoryStream(); + using (var zlib = new ZLibStream(compressed, CompressionLevel.SmallestSize, leaveOpen: true)) + using (var writer = new StreamWriter(zlib)) + { + writer.Write(xml); + } + + var truncated = compressed.ToArray()[..^4]; + var decompressor = new BlobDecompressor(); + + var result = decompressor.decompress(truncated, 0); + + Assert.Equal(xml, result); + } + + [Fact] + public async Task BrowseBlockStructureParsesPlcStructureXml() + { + const string xml = """ + + + + + + + + 123 + + + + + Default tag table + + + + + + + +
+ + + + + + + + + + + + """; + var fake = new FakeS7CommPlusSession + { + PlcStructureXmlHandler = () => (0, xml) + }; + var client = CreateClient(fake); + + var snapshot = await client.GetPlcStructureXmlAsync(); + var structure = await client.BrowseBlockStructureAsync(); + + Assert.Equal(xml, snapshot.Xml); + Assert.Equal(ExpectedSha256Hex(xml), snapshot.ProgramChangeMarker.StructureHash); + Assert.Equal(new DateTime(2026, 5, 22, 0, 1, 0, DateTimeKind.Utc), snapshot.ProgramChangeMarker.LastModified); + Assert.Equal(1, snapshot.ProgramChangeMarker.BlockCount); + Assert.Equal(1, snapshot.ProgramChangeMarker.TagTableCount); + var unit = Assert.Single(structure); + Assert.Equal(S7CommPlusPlcStructureNodeKind.Unit, unit.Kind); + Assert.Equal("Software unit", unit.Name); + var programBlocks = unit.Children.Single(child => child.Name == "Program blocks"); + var mainFolder = Assert.Single(programBlocks.Children); + var block = Assert.Single(mainFolder.Children); + Assert.Equal(S7CommPlusPlcStructureNodeKind.Block, block.Kind); + Assert.Equal((uint)123, block.RelationId); + Assert.Equal("MainBlock", block.Name); + Assert.Equal(1, block.Number); + Assert.Equal("FB", block.BlockType); + Assert.Equal("SCL", block.BlockLanguage); + var tagTable = Assert.Single(unit.Children.Single(child => child.Name == "PLC tags").Children.Single().Children); + Assert.Equal(S7CommPlusPlcStructureNodeKind.Item, tagTable.Kind); + Assert.Equal("Default tag table", tagTable.Name); + Assert.Equal("TagTable", tagTable.BlockType); + } + + private static string ExpectedSha256Hex(string value) + { + using var sha = SHA256.Create(); + return BitConverter.ToString(sha.ComputeHash(Encoding.UTF8.GetBytes(value))).Replace("-", string.Empty); + } + + [Fact] + public async Task ConcurrentReadsAreSerialized() + { + var fake = new FakeS7CommPlusSession + { + ReadHandler = _ => + { + Task.Delay(50).Wait(); + return (0, new List { new ValueInt(1) }, new List { 0 }); + } + }; + var client = CreateClient(fake); + var addresses = new[] { new ItemAddress("8A0E0001.F") }; + + await Task.WhenAll(Enumerable.Range(0, 5).Select(_ => client.ReadAsync(addresses))); + + Assert.Equal(1, fake.MaxConcurrentReads); + Assert.Equal(5, fake.ReadCount); + } + + [Fact] + public async Task WritesAreBlockedByDefault() + { + var fake = new FakeS7CommPlusSession(); + var client = CreateClient(fake); + + await Assert.ThrowsAsync(() => + client.WriteAsync(new[] { new ItemAddress("8A0E0001.F") }, new PValue[] { new ValueInt(1) })); + } + + [Fact] + public async Task CpuStartStopAreBlockedByDefault() + { + var fake = new FakeS7CommPlusSession(); + var client = CreateClient(fake); + + await Assert.ThrowsAsync(() => client.StopCpuAsync()); + await Assert.ThrowsAsync(() => client.StartCpuAsync()); + + Assert.Equal(0, fake.CpuOperatingStateWriteCount); + } + + [Fact] + public async Task CpuStartStopUseOperatingStateRequests() + { + var fake = new FakeS7CommPlusSession(); + var client = CreateClient(fake, writeEnabled: true); + + await client.StopCpuAsync(); + await client.StartCpuAsync(); + + Assert.Equal(new[] { 1, 3 }, fake.CpuOperatingStateRequests); + Assert.Equal(S7CommPlusConnectionState.Connected, client.State); + } + + [Fact] + public async Task SecurityModeIsPassedToConnectionAndNegotiatedModeIsExposed() + { + var fake = new FakeS7CommPlusSession + { + ConnectHandler = options => + { + Assert.Equal(S7CommPlusSecurityMode.LegacyChallenge, options.SecurityMode); + Assert.Equal(S7CommPlusTlsBackend.BouncyCastle, options.TlsBackend); + options.NegotiatedSecurityMode = S7CommPlusSecurityMode.LegacyChallenge; + return 0; + } + }; + var client = new S7CommPlusClient( + new S7CommPlusClientOptions + { + Address = "127.0.0.1", + SecurityMode = S7CommPlusSecurityMode.LegacyChallenge, + TlsBackend = S7CommPlusTlsBackend.BouncyCastle, + ConnectTimeout = TimeSpan.FromMilliseconds(500), + RequestTimeout = TimeSpan.FromMilliseconds(500), + DisconnectTimeout = TimeSpan.FromMilliseconds(100) + }, + () => fake); + + await client.ConnectAsync(); + + Assert.Equal(S7CommPlusSecurityMode.LegacyChallenge, client.Options.NegotiatedSecurityMode); + Assert.Equal(S7CommPlusTlsBackend.BouncyCastle, client.Options.TlsBackend); + } + + [Fact] + public void BouncyCastleIsDefaultTlsBackend() + { + var options = new S7CommPlusClientOptions(); + + Assert.Equal(S7CommPlusTlsBackend.BouncyCastle, options.TlsBackend); + } + + [Fact] + public async Task ConnectErrorIncludesSessionErrorDetail() + { + var fake = new FakeS7CommPlusSession + { + LastErrorDetail = "Could not load native OpenSSL dependency 'runtimes/win-arm64/native/libcrypto-3-arm64.dll'." + }; + fake.ConnectHandler = _ => S7Consts.errOpenSSL; + var client = CreateClient(fake); + + var ex = await Assert.ThrowsAsync(() => client.ConnectAsync()); + + Assert.Contains("OPENSSL", ex.Message); + Assert.Contains("libcrypto-3-arm64.dll", ex.Message); + } + + [Fact] + public void RemoteTsapMustBeAscii() + { + var ex = Assert.Throws(() => new S7CommPlusClient( + new S7CommPlusClientOptions + { + Address = "127.0.0.1", + RemoteTsap = "SIMATIC-ROOT-HMI-\u00C4" + }, + () => new FakeS7CommPlusSession())); + + Assert.Equal("RemoteTsap", ex.ParamName); + } + + [Fact] + public void RemoteTsapMustFitCotpParameterLength() + { + var ex = Assert.Throws(() => new S7CommPlusClient( + new S7CommPlusClientOptions + { + Address = "127.0.0.1", + RemoteTsap = new string('A', 256) + }, + () => new FakeS7CommPlusSession())); + + Assert.Equal("RemoteTsap", ex.ParamName); + } + + [Fact] + public void SiemensTsapDefaultsAreAvailable() + { + Assert.Equal(102, S7CommPlusDefaults.IsoTcpPort); + Assert.Equal((ushort)0x0600, S7CommPlusDefaults.LocalTsap); + Assert.Equal("SIMATIC-ROOT-HMI", S7CommPlusDefaults.RemoteTsapHmi); + Assert.Equal("SIMATIC-ROOT-ES", S7CommPlusDefaults.RemoteTsapEs); + } + + [Fact] + public async Task LegitimateAsyncUsesTypedLegitimationErrorAndDoesNotRequireWriteEnablement() + { + var fake = new FakeS7CommPlusSession + { + LegitimateHandler = (password, username) => + { + Assert.Equal("secret", password); + Assert.Equal("operator", username); + return S7Consts.errCliAccessDenied; + } + }; + var client = CreateClient(fake); + + var ex = await Assert.ThrowsAsync(() => + client.LegitimateAsync("secret", "operator")); + + Assert.Equal("Legitimate", ex.Operation); + Assert.False(ex.IsTransient); + } + + [Fact] + public async Task GetCommunicationResourcesUsesReadPipeline() + { + var fake = new FakeS7CommPlusSession(); + var client = CreateClient(fake); + + var resources = await client.GetCommunicationResourcesAsync(); + + Assert.Equal(20, resources.TagsPerReadRequestMax); + Assert.Equal(20, resources.TagsPerWriteRequestMax); + Assert.Equal(S7CommPlusConnectionState.Connected, client.State); + } + + [Fact] + public async Task GetCpuStateUsesReadPipeline() + { + var fake = new FakeS7CommPlusSession + { + CpuStateHandler = () => (0, new S7CommPlusCpuState(5, S7CommPlusCpuOperatingState.Run, 2, "Run")) + }; + var client = CreateClient(fake); + + var cpuState = await client.GetCpuStateAsync(); + + Assert.Equal(5, cpuState.RawOperatingState); + Assert.Equal(S7CommPlusCpuOperatingState.Run, cpuState.OperatingState); + Assert.Equal(2, cpuState.RawStateSwitch); + Assert.Equal("Run", cpuState.StateSwitch); + Assert.True(cpuState.IsRun); + Assert.False(cpuState.IsStop); + Assert.Equal(S7CommPlusConnectionState.Connected, client.State); + } + + [Fact] + public async Task GetCpuCycleTimeUsesReadPipeline() + { + var fake = new FakeS7CommPlusSession + { + CpuCycleTimeHandler = () => (0, new S7CommPlusCpuCycleTime(0, 150, 50.007, 50.012, 50.654)) + }; + var client = CreateClient(fake); + + var cycleTime = await client.GetCpuCycleTimeAsync(); + + Assert.Equal(0, cycleTime.ConfiguredMinimumMilliseconds); + Assert.Equal(150, cycleTime.ConfiguredMaximumMilliseconds); + Assert.Equal(50.007, cycleTime.ShortestMilliseconds); + Assert.Equal(50.012, cycleTime.CurrentMilliseconds); + Assert.Equal(50.654, cycleTime.LongestMilliseconds); + Assert.Equal(S7CommPlusConnectionState.Connected, client.State); + } + + [Fact] + public async Task GetCpuMemoryUsageUsesReadPipeline() + { + var fake = new FakeS7CommPlusSession + { + CpuMemoryUsageHandler = () => (0, new S7CommPlusCpuMemoryUsage(new[] + { + new S7CommPlusCpuMemoryArea("load", "Load memory", 1000, 120), + new S7CommPlusCpuMemoryArea("retain", "Retain memory", 500, 25) + })) + }; + var client = CreateClient(fake); + + var memoryUsage = await client.GetCpuMemoryUsageAsync(); + + Assert.Collection( + memoryUsage.Areas, + area => + { + Assert.Equal("load", area.Key); + Assert.Equal("Load memory", area.Name); + Assert.Equal(1000, area.TotalBytes); + Assert.Equal(120, area.UsedBytes); + Assert.Equal(880, area.FreeBytes); + Assert.Equal(12, area.UsedPercent); + Assert.Equal(88, area.FreePercent); + }, + area => + { + Assert.Equal("retain", area.Key); + Assert.Equal(500, area.TotalBytes); + Assert.Equal(25, area.UsedBytes); + }); + Assert.Equal(S7CommPlusConnectionState.Connected, client.State); + } + + [Fact] + public async Task GetCpuCultureInfoUsesReadPipeline() + { + var fake = new FakeS7CommPlusSession + { + CpuCultureInfoHandler = () => (0, new S7CommPlusCpuCultureInfo(new[] { 1031, 1033 })) + }; + var client = CreateClient(fake); + + var cultureInfo = await client.GetCpuCultureInfoAsync(); + + Assert.Equal(new[] { 1031, 1033 }, cultureInfo.LanguageIds); + Assert.Equal("de-DE", cultureInfo.PrimaryCulture.Name); + Assert.Contains(cultureInfo.Cultures, culture => culture.Name == "en-US"); + Assert.Equal(S7CommPlusConnectionState.Connected, client.State); + } + + [Fact] + public void CpuCultureInfoPreservesUnresolvedLanguageIds() + { + var cultureInfo = new S7CommPlusCpuCultureInfo(new[] { 1031, 0xffff }); + + Assert.Equal(new[] { 1031, 0xffff }, cultureInfo.LanguageIds); + Assert.Equal(new[] { 0xffff }, cultureInfo.UnresolvedLanguageIds); + Assert.Equal("de-DE", cultureInfo.PrimaryCulture.Name); + } + + [Fact] + public async Task GetActiveAlarmsUsesReadPipeline() + { + var fake = new FakeS7CommPlusSession + { + ActiveAlarmsHandler = () => (0, new List()) + }; + var client = CreateClient(fake); + + var alarms = await client.GetActiveAlarmsAsync(languageId: 1033); + + Assert.Empty(alarms); + Assert.Equal(1033, fake.LastActiveAlarmsLanguageId); + Assert.Equal(S7CommPlusConnectionState.Connected, client.State); + } + + [Fact] + public async Task GetActiveAlarmsWithoutLanguageIdRequestsAllLanguages() + { + var fake = new FakeS7CommPlusSession + { + ActiveAlarmsHandler = () => (0, new List()) + }; + var client = CreateClient(fake); + + var alarms = await client.GetActiveAlarmsAsync(); + + Assert.Empty(alarms); + Assert.Equal(0, fake.LastActiveAlarmsLanguageId); + } + + [Fact] + public void AlarmExposesSourceRelationAndAlarmIds() + { + var alarm = new S7CommPlusAlarm + { + CpuAlarmId = ((ulong)0x12345678 << 32) | ((ulong)0x9abc << 16) + }; + + Assert.Equal((uint)0x12345678, alarm.SourceRelationId); + Assert.Equal((ushort)0x9abc, alarm.SourceAlarmId); + } + + [Fact] + public void AlarmTextsCanReadAllNotificationLanguages() + { + var blob = new ValueBlobSparseArray(new Dictionary + { + { ((uint)1031 << 16) | 2, BlobText("Alarm deutsch") }, + { ((uint)1033 << 16) | 2, BlobText("Alarm english") }, + { ((uint)1033 << 16) | 1, BlobText("Info english") } + }); + + var texts = S7CommPlusAlarmTexts.FromNotificationBlobAllLanguages(blob); + + Assert.Equal("Alarm deutsch", texts[1031].AlarmText); + Assert.Equal("Alarm english", texts[1033].AlarmText); + Assert.Equal("Info english", texts[1033].Infotext); + } + + [Fact] + public async Task TagSubscriptionPublishesValuesAndDeletesOnDispose() + { + var allowNotifications = new ManualResetEventSlim(false); + var fake = new FakeS7CommPlusSession(); + fake.WaitForTagSubscriptionHandler = (_, _) => + { + allowNotifications.Wait(TimeSpan.FromSeconds(1)); + if (fake.TagSubscriptionWaitCount == 1) + { + return (0, new List + { + new Notification(2) + { + Add1Timestamp = DateTime.UtcNow, + NotificationCreditTick = 1, + NotificationSequenceNumber = 7, + Values = { [1] = new ValueInt(42) } + } + }); + } + + Thread.Sleep(10); + return (S7Consts.errCliJobTimeout, new List()); + }; + var client = CreateClient(fake); + var tag = PlcTags.TagFactory("DB.Value", new ItemAddress("8A0E0001.F"), Softdatatype.S7COMMP_SOFTDATATYPE_INT); + var received = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await using var subscription = await client.SubscribeTagsAsync(new[] { tag }, FastSubscriptionOptions()); + subscription.NotificationReceived += (_, args) => received.TrySetResult(args.Notification); + allowNotifications.Set(); + + var notification = await received.Task.WaitAsync(TimeSpan.FromSeconds(2)); + await subscription.StopAsync(); + + Assert.Equal((uint)7, notification.SequenceNumber); + Assert.Single(notification.Items); + Assert.True(notification.Items[0].IsSuccess); + Assert.Same(tag, notification.Items[0].Tag); + Assert.Equal(1, fake.TagSubscriptionCreateCount); + Assert.Equal(1, fake.TagSubscriptionDeleteCount); + } + + [Fact] + public void TisResultParserUsesRequestModelOffsets() + { + var model = new S7CommPlusTisResultModel(); + var watchPoint = new S7CommPlusTisWatchPointModel + { + NetworkId = "cu-1", + Uid = "21", + Pin = "operand", + RloOffset = 20 + }; + watchPoint.Values.Add(new S7CommPlusTisValueModel + { + NetworkId = "cu-1", + Uid = "21", + Pin = "operand", + ValueOffset = 30, + ByteLength = 1, + ValidityOffset = 31, + ValidCountOffset = 32 + }); + model.WatchPoints.Add(watchPoint); + var result = new byte[40]; + result[20] = 0x80; + result[23] = 0x05; + result[30] = 1; + result[31] = 0x7f; + result[33] = 2; + + var parsed = S7CommPlusTisResultParser.Parse(result, model); + + var parsedWatchPoint = Assert.Single(parsed); + Assert.True(parsedWatchPoint.Rlo); + Assert.Equal((uint)5, parsedWatchPoint.ExecutionCount); + var parsedValue = Assert.Single(parsedWatchPoint.Values); + Assert.True(parsedValue.BoolValue); + Assert.Equal((byte)0x7f, parsedValue.Validity); + Assert.Equal((ushort)2, parsedValue.ValidCount); + Assert.Equal("TRUE", parsedValue.DisplayValue); + } + + [Fact] + public void NotificationParsesCapturedOnlineStatusTableItems() + { + using var stream = CreateNotificationBody(); + stream.WriteByte((byte)S7CommPlusNotificationReturnCode.OnlineStatusTable); + stream.WriteByte(0x74); + stream.WriteByte(0x00); + stream.WriteByte(0x08); + stream.WriteByte(0x02); + stream.WriteByte((byte)S7CommPlusNotificationReturnCode.OnlineStatusTable); + stream.WriteByte(0x70); + stream.WriteByte(0x00); + stream.WriteByte(0x08); + stream.WriteByte(0x01); + stream.WriteByte((byte)S7CommPlusNotificationReturnCode.EndOfList); + stream.WriteByte(0); + stream.Position = 0; + + var notification = new Notification(2); + notification.Deserialize(stream); + + Assert.Equal(0x74000802u, notification.OnlineStatusTableValues[0x74]); + Assert.Equal(0x70000801u, notification.OnlineStatusTableValues[0x70]); + Assert.Empty(notification.Values); + } + + [Fact] + public void NotificationParsesCapturedTisResultAsBlobValue() + { + var result = new byte[116]; + result[104] = 0x80; + result[107] = 0x01; + result[108] = 0x00; + result[111] = 0x02; + result[112] = 0x80; + result[115] = 0x03; + using var stream = CreateNotificationBody(); + stream.WriteByte((byte)S7CommPlusNotificationReturnCode.ValueWithUInt32Reference); + WriteUInt32(stream, 9); + new ValueBlob(0, result).Serialize(stream); + stream.WriteByte((byte)S7CommPlusNotificationReturnCode.ValueWithUInt32Reference); + WriteUInt32(stream, 10); + new ValueUSInt(1).Serialize(stream); + stream.WriteByte((byte)S7CommPlusNotificationReturnCode.ValueWithUInt32Reference); + WriteUInt32(stream, 11); + new ValueBool(true).Serialize(stream); + stream.WriteByte((byte)S7CommPlusNotificationReturnCode.EndOfList); + stream.WriteByte(0); + stream.Position = 0; + + var notification = new Notification(2); + notification.Deserialize(stream); + var model = new S7CommPlusTisResultModel(); + model.WatchPoints.Add(new S7CommPlusTisWatchPointModel { RloOffset = 104 }); + model.WatchPoints.Add(new S7CommPlusTisWatchPointModel { RloOffset = 108 }); + model.WatchPoints.Add(new S7CommPlusTisWatchPointModel { RloOffset = 112 }); + + var blob = Assert.IsType(notification.Values[9]); + var parsed = S7CommPlusTisResultParser.Parse(blob.GetValue(), model); + + Assert.True(parsed[0].Rlo); + Assert.False(parsed[1].Rlo); + Assert.True(parsed[2].Rlo); + Assert.Equal((byte)1, Assert.IsType(notification.Values[10]).GetValue()); + Assert.True(Assert.IsType(notification.Values[11]).GetValue()); + } + + [Fact] + public async Task TisWatchSubscriptionPublishesUpdatesAndDeletesOnDispose() + { + var allowNotifications = new ManualResetEventSlim(false); + var fake = new FakeS7CommPlusSession(); + var notification = new S7CommPlusTisWatchNotification( + DateTime.UtcNow, + 123, + 1, + true, + 1, + new byte[] { 1, 2, 3 }, + new[] + { + new S7CommPlusTisWatchPointResult("cu-1", "21", "operand", true, 0x80000001, 1, Array.Empty()) + }); + fake.WaitForTisWatchSubscriptionHandler = _ => + { + allowNotifications.Wait(TimeSpan.FromSeconds(1)); + if (fake.TisWatchSubscriptionWaitCount == 1) + { + return (0, new List { notification }); + } + + Thread.Sleep(10); + return (S7Consts.errCliJobTimeout, new List()); + }; + var client = CreateClient(fake); + var received = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await using var subscription = await client.OpenBlockOnlineViewAsync(new S7CommPlusTisWatchRequest + { + RequestBlob = new byte[] { 0x40, 0x80, 0x32, 0x06 }, + TriggerBlob = new byte[] { 0x10, 0x06 }, + }, FastSubscriptionOptions()); + subscription.NotificationReceived += (_, args) => received.TrySetResult(args.Notification); + allowNotifications.Set(); + + var update = await received.Task.WaitAsync(TimeSpan.FromSeconds(2)); + await subscription.StopAsync(); + + Assert.Equal((uint)123, update.SequenceNumber); + Assert.Single(update.WatchPoints); + Assert.True(update.WatchPoints[0].Rlo); + Assert.Equal(1, fake.TisWatchSubscriptionCreateCount); + Assert.Equal(1, fake.TisWatchSubscriptionDeleteCount); + } + + [Fact] + public async Task MultipleTisWatchSubscriptionsUseOneConnectionAndDistinctHandles() + { + var fake = new FakeS7CommPlusSession + { + WaitForTisWatchSubscriptionByIdHandler = (_, _) => + { + Thread.Sleep(5); + return (S7Consts.errCliJobTimeout, new List()); + } + }; + var client = CreateClient(fake); + var options = FastSubscriptionOptions(); + + await using var first = await client.OpenBlockOnlineViewAsync(CreateTisWatchRequest(1), options); + await using var second = await client.OpenBlockOnlineViewAsync(CreateTisWatchRequest(2), options); + await using var third = await client.OpenBlockOnlineViewAsync(CreateTisWatchRequest(3), options); + + await first.StopAsync(); + await second.StopAsync(); + await third.StopAsync(); + + Assert.Equal(1, fake.ConnectCount); + Assert.Equal(3, fake.TisWatchSubscriptionCreateCount); + Assert.Equal(3, fake.TisWatchSubscriptionDeleteCount); + Assert.Equal(3, fake.CreatedTisWatchSubscriptionIds.Distinct().Count()); + Assert.Equal( + fake.CreatedTisWatchSubscriptionIds.OrderBy(id => id), + fake.DeletedTisWatchSubscriptionIds.OrderBy(id => id)); + } + + [Fact] + public async Task TagSubscriptionPublishesPerItemErrors() + { + var allowNotifications = new ManualResetEventSlim(false); + var fake = new FakeS7CommPlusSession(); + fake.WaitForTagSubscriptionHandler = (_, _) => + { + allowNotifications.Wait(TimeSpan.FromSeconds(1)); + if (fake.TagSubscriptionWaitCount == 1) + { + return (0, new List + { + new Notification(2) + { + ReturnValues = { [1] = 0x13 } + } + }); + } + + Thread.Sleep(10); + return (S7Consts.errCliJobTimeout, new List()); + }; + var client = CreateClient(fake); + var tag = PlcTags.TagFactory("DB.Value", new ItemAddress("8A0E0001.F"), Softdatatype.S7COMMP_SOFTDATATYPE_INT); + var received = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await using var subscription = await client.SubscribeTagsAsync(new[] { tag }, FastSubscriptionOptions()); + subscription.NotificationReceived += (_, args) => received.TrySetResult(args.Notification); + allowNotifications.Set(); + + var notification = await received.Task.WaitAsync(TimeSpan.FromSeconds(2)); + await subscription.StopAsync(); + + Assert.Single(notification.Items); + Assert.False(notification.Items[0].IsSuccess); + Assert.Equal((ulong)0x13, notification.Items[0].ItemError); + Assert.Equal(1, fake.TagSubscriptionDeleteCount); + } + + [Fact] + public async Task SubscriptionIdleTimeoutDoesNotFaultByDefault() + { + var fake = new FakeS7CommPlusSession + { + WaitForTagSubscriptionHandler = (_, _) => + { + Thread.Sleep(5); + return (S7Consts.errCliJobTimeout, new List()); + } + }; + var client = CreateClient(fake); + var tag = PlcTags.TagFactory("DB.Value", new ItemAddress("8A0E0001.F"), Softdatatype.S7COMMP_SOFTDATATYPE_INT); + + await using var subscription = await client.SubscribeTagsAsync(new[] { tag }, FastSubscriptionOptions()); + await Task.Delay(40); + await subscription.StopAsync(); + + Assert.Equal(S7CommPlusSubscriptionState.Stopped, subscription.State); + Assert.Equal(1, fake.TagSubscriptionDeleteCount); + } + + [Fact] + public async Task ReadCanRunWhileSubscriptionIsActive() + { + var fake = new FakeS7CommPlusSession + { + WaitForTagSubscriptionHandler = (_, _) => + { + Thread.Sleep(10); + return (S7Consts.errCliJobTimeout, new List()); + } + }; + var client = CreateClient(fake); + var tag = PlcTags.TagFactory("DB.Value", new ItemAddress("8A0E0001.F"), Softdatatype.S7COMMP_SOFTDATATYPE_INT); + + await using var subscription = await client.SubscribeTagsAsync(new[] { tag }, FastSubscriptionOptions()); + var readTask = client.ReadAsync(new[] { tag }); + await Task.Delay(40); + + var read = await readTask.WaitAsync(TimeSpan.FromSeconds(2)); + await subscription.StopAsync(); + + Assert.Single(read.Items); + Assert.Equal(1, fake.ReadCount); + } + + [Fact] + public async Task AlarmAndTisSubscriptionsCanBeCreatedOnSameClientConnection() + { + var fake = new FakeS7CommPlusSession + { + WaitForAlarmSubscriptionHandler = (_, _) => + { + Thread.Sleep(5); + return (S7Consts.errCliJobTimeout, new List()); + }, + WaitForTisWatchSubscriptionHandler = _ => + { + Thread.Sleep(5); + return (S7Consts.errCliJobTimeout, new List()); + } + }; + var client = CreateClient(fake); + + await using var alarmSubscription = await client.SubscribeAlarmsAsync( + options: new S7CommPlusSubscriptionOptions + { + NotificationTimeout = TimeSpan.FromMilliseconds(20) + }); + await using var tisSubscription = await client.OpenBlockOnlineViewAsync(new S7CommPlusTisWatchRequest + { + RequestBlob = new byte[] { 0x40, 0x80, 0x32, 0x06 }, + TriggerBlob = new byte[] { 0x10, 0x06 }, + }, FastSubscriptionOptions()); + + await alarmSubscription.StopAsync(); + await tisSubscription.StopAsync(); + + Assert.Equal(1, fake.ConnectCount); + Assert.Equal(1, fake.AlarmSubscriptionCreateCount); + Assert.Equal(1, fake.TisWatchSubscriptionCreateCount); + Assert.Equal(1, fake.AlarmSubscriptionDeleteCount); + Assert.Equal(1, fake.TisWatchSubscriptionDeleteCount); + } + + [Fact] + public async Task ActiveAlarmsCanBeReadAfterAlarmSubscriptionStartsOnSameConnection() + { + var fake = new FakeS7CommPlusSession + { + ActiveAlarmsHandler = () => (0, new List()), + WaitForAlarmSubscriptionHandler = (_, _) => + { + Thread.Sleep(5); + return (S7Consts.errCliJobTimeout, new List()); + } + }; + var client = CreateClient(fake); + + await using var subscription = await client.SubscribeAlarmsAsync( + 1033, + new S7CommPlusSubscriptionOptions + { + NotificationTimeout = TimeSpan.FromMilliseconds(20) + }); + + var activeAlarms = await client.GetActiveAlarmsAsync(1033); + await subscription.StopAsync(); + + Assert.Empty(activeAlarms); + Assert.Equal(1, fake.ConnectCount); + Assert.Equal(1, fake.AlarmSubscriptionCreateCount); + Assert.Equal(1, fake.AlarmSubscriptionDeleteCount); + Assert.Equal(1033, fake.LastActiveAlarmsLanguageId); + Assert.Equal(S7CommPlusConnectionState.Connected, client.State); + } + + [Fact] + public async Task SubscriptionCommunicationFailureFaultsSubscriptionAndClient() + { + var fake = new FakeS7CommPlusSession + { + WaitForTagSubscriptionHandler = (_, _) => (S7Consts.errTCPDataReceive, new List()) + }; + var client = CreateClient(fake); + var tag = PlcTags.TagFactory("DB.Value", new ItemAddress("8A0E0001.F"), Softdatatype.S7COMMP_SOFTDATATYPE_INT); + var error = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await using var subscription = await client.SubscribeTagsAsync(new[] { tag }, FastSubscriptionOptions()); + subscription.CommunicationError += (_, args) => error.TrySetResult(args.Exception); + + var ex = await error.Task.WaitAsync(TimeSpan.FromSeconds(2)); + + Assert.Equal("WaitForTagSubscriptionNotifications", ex.Operation); + Assert.Equal(S7CommPlusSubscriptionState.Faulted, subscription.State); + Assert.Equal(S7CommPlusConnectionState.Faulted, client.State); + await Assert.ThrowsAsync(() => subscription.Completion); + Assert.Equal(1, fake.TagSubscriptionDeleteCount); + } + + [Fact] + public async Task AlarmSubscriptionCreatesWithLanguagesAndDeletesOnDispose() + { + uint[]? capturedLanguages = null; + var fake = new FakeS7CommPlusSession + { + CreateAlarmSubscriptionHandler = (languages, creditLimit) => + { + capturedLanguages = languages; + Assert.Equal((short)3, creditLimit); + return 0; + }, + WaitForAlarmSubscriptionHandler = (_, _) => + { + Thread.Sleep(5); + return (S7Consts.errCliJobTimeout, new List()); + } + }; + var client = CreateClient(fake); + + await using var subscription = await client.SubscribeAlarmsAsync( + new[] { 1031, 1033 }, + alarmTextLanguageId: 1033, + options: new S7CommPlusSubscriptionOptions + { + InitialCreditLimit = 3, + NotificationTimeout = TimeSpan.FromMilliseconds(20) + }); + await subscription.StopAsync(); + + Assert.Equal(new uint[] { 1031, 1033 }, capturedLanguages); + Assert.Equal(1033, subscription.AlarmTextLanguageId); + Assert.Equal(1, fake.AlarmSubscriptionCreateCount); + Assert.Equal(1, fake.AlarmSubscriptionDeleteCount); + Assert.Equal(0, fake.DisconnectCount); + Assert.Equal(S7CommPlusConnectionState.Connected, client.State); + } + + [Fact] + public async Task AlarmSubscriptionWithoutLanguageIdRequestsAllLanguages() + { + uint[]? capturedLanguages = null; + var fake = new FakeS7CommPlusSession + { + CreateAlarmSubscriptionHandler = (languages, _) => + { + capturedLanguages = languages; + return 0; + }, + WaitForAlarmSubscriptionHandler = (_, _) => + { + Thread.Sleep(5); + return (S7Consts.errCliJobTimeout, new List()); + } + }; + var client = CreateClient(fake); + + await using var subscription = await client.SubscribeAlarmsAsync( + options: new S7CommPlusSubscriptionOptions + { + NotificationTimeout = TimeSpan.FromMilliseconds(20) + }); + await subscription.StopAsync(); + + Assert.Empty(capturedLanguages!); + Assert.True(subscription.ReceivesAllAlarmTextLanguages); + Assert.Equal(0, subscription.AlarmTextLanguageId); + } + + [Fact] + public async Task AlarmSubscriptionConnectionLossSkipsProtocolDelete() + { + var fake = new FakeS7CommPlusSession + { + WaitForAlarmSubscriptionHandler = (_, _) => (S7Consts.errTCPNotConnected, new List()) + }; + var client = CreateClient(fake); + var error = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await using var subscription = await client.SubscribeAlarmsAsync( + languageId: 1031, + options: new S7CommPlusSubscriptionOptions + { + NotificationTimeout = TimeSpan.FromMilliseconds(20) + }); + subscription.CommunicationError += (_, args) => error.TrySetResult(args.Exception); + + var ex = await error.Task.WaitAsync(TimeSpan.FromSeconds(2)); + + Assert.Equal("WaitForAlarmNotifications", ex.Operation); + Assert.Equal(S7Consts.errTCPNotConnected, ex.ErrorCode); + Assert.Equal(S7CommPlusSubscriptionState.Faulted, subscription.State); + Assert.Equal(S7CommPlusConnectionState.Faulted, client.State); + await Assert.ThrowsAsync(() => subscription.Completion); + Assert.Equal(1, fake.AlarmSubscriptionCreateCount); + Assert.Equal(0, fake.AlarmSubscriptionDeleteCount); + } + + [Fact] + public async Task AlarmSnapshotSubscriptionSubscribesBeforeReadingActiveAlarms() + { + var order = new List(); + var subscriptionFake = new FakeS7CommPlusSession + { + CreateAlarmSubscriptionHandler = (_, _) => + { + order.Add("subscribe"); + return 0; + }, + WaitForAlarmSubscriptionHandler = (_, _) => (S7Consts.errCliJobTimeout, new List()) + }; + var snapshotFake = new FakeS7CommPlusSession + { + ActiveAlarmsHandler = () => + { + order.Add("snapshot"); + return (0, new List()); + } + }; + var subscriptionClient = CreateClient(subscriptionFake); + var snapshotClient = CreateClient(snapshotFake); + + await using var result = await subscriptionClient.SubscribeAlarmsWithSnapshotAsync( + snapshotClient, + languageId: 1031, + options: new S7CommPlusSubscriptionOptions + { + NotificationTimeout = TimeSpan.FromMilliseconds(20) + }); + await result.Subscription.StopAsync(); + + Assert.Same(result.Subscription, result.Subscription); + Assert.Empty(result.ActiveAlarms); + Assert.Equal(new[] { "subscribe", "snapshot" }, order); + Assert.Equal(1031, snapshotFake.LastActiveAlarmsLanguageId); + Assert.Equal(1, subscriptionFake.AlarmSubscriptionCreateCount); + Assert.Equal(1, snapshotFake.ConnectCount); + } + + [Fact] + public async Task AlarmSnapshotWithoutLanguageIdRequestsAllLanguages() + { + uint[]? capturedLanguages = null; + var subscriptionFake = new FakeS7CommPlusSession + { + CreateAlarmSubscriptionHandler = (languages, _) => + { + capturedLanguages = languages; + return 0; + }, + WaitForAlarmSubscriptionHandler = (_, _) => (S7Consts.errCliJobTimeout, new List()) + }; + var snapshotFake = new FakeS7CommPlusSession + { + ActiveAlarmsHandler = () => (0, new List()) + }; + var subscriptionClient = CreateClient(subscriptionFake); + var snapshotClient = CreateClient(snapshotFake); + + await using var result = await subscriptionClient.SubscribeAlarmsWithSnapshotAsync( + snapshotClient, + options: new S7CommPlusSubscriptionOptions + { + NotificationTimeout = TimeSpan.FromMilliseconds(20) + }); + await result.Subscription.StopAsync(); + + Assert.Empty(capturedLanguages!); + Assert.Equal(0, snapshotFake.LastActiveAlarmsLanguageId); + Assert.True(result.Subscription.ReceivesAllAlarmTextLanguages); + } + + private static S7CommPlusClient CreateClient(FakeS7CommPlusSession fake, int requestTimeoutMs = 5000, bool writeEnabled = false) + { + return new S7CommPlusClient( + new S7CommPlusClientOptions + { + Address = "127.0.0.1", + WriteEnabled = writeEnabled, + RequestTimeout = TimeSpan.FromMilliseconds(requestTimeoutMs), + ConnectTimeout = TimeSpan.FromMilliseconds(500), + DisconnectTimeout = TimeSpan.FromMilliseconds(100) + }, + () => fake); + } + + private static S7CommPlusSubscriptionOptions FastSubscriptionOptions() + { + return new S7CommPlusSubscriptionOptions + { + CycleTimeMilliseconds = 100, + NotificationTimeout = TimeSpan.FromMilliseconds(20) + }; + } + + private static S7CommPlusTisWatchRequest CreateTisWatchRequest(byte seed) + { + return new S7CommPlusTisWatchRequest + { + RequestBlob = new byte[] { 0x40, 0x80, 0x32, seed }, + TriggerBlob = new byte[] { 0x10, seed }, + }; + } + + private static MemoryStream CreateNotificationBody() + { + var stream = new MemoryStream(); + WriteUInt32(stream, 0x70000FFB); + WriteUInt16(stream, 0); + WriteUInt16(stream, 0); + WriteUInt16(stream, 0); + stream.WriteByte(1); + stream.WriteByte(1); + stream.WriteByte(1); + return stream; + } + + private static void WriteUInt16(Stream stream, ushort value) + { + stream.WriteByte((byte)(value >> 8)); + stream.WriteByte((byte)value); + } + + private static void WriteUInt32(Stream stream, uint value) + { + stream.WriteByte((byte)(value >> 24)); + stream.WriteByte((byte)(value >> 16)); + stream.WriteByte((byte)(value >> 8)); + stream.WriteByte((byte)value); + } + + private static ValueBlobSparseArray.BlobEntry BlobText(string text) + { + return new ValueBlobSparseArray.BlobEntry + { + blobRootId = 0, + value = Encoding.UTF8.GetBytes(text) + }; + } + } +} diff --git a/src/S7CommPlusDriver.Tests/S7CommPlusDriver.Tests.csproj b/src/S7CommPlusDriver.Tests/S7CommPlusDriver.Tests.csproj new file mode 100644 index 0000000..0aaf60e --- /dev/null +++ b/src/S7CommPlusDriver.Tests/S7CommPlusDriver.Tests.csproj @@ -0,0 +1,16 @@ + + + net8.0 + false + enable + + + + + + + + + + + diff --git a/src/S7CommPlusDriver.Tests/S7CommPlusProtocolSessionReceiveTests.cs b/src/S7CommPlusDriver.Tests/S7CommPlusProtocolSessionReceiveTests.cs new file mode 100644 index 0000000..0e46ad9 --- /dev/null +++ b/src/S7CommPlusDriver.Tests/S7CommPlusProtocolSessionReceiveTests.cs @@ -0,0 +1,229 @@ +using System; +using System.Linq; +using System.IO; +using System.Security.Cryptography; +using Xunit; + +namespace S7CommPlusDriver.Tests +{ + public sealed class S7CommPlusProtocolSessionReceiveTests + { + [Fact] + public void MalformedPduPublishesReceiveError() + { + var connection = new S7CommPlusProtocolSession(); + connection.DebugResetReceiveDispatcherForTests(); + + connection.DebugOnDataReceivedForTests(new byte[] { 0x00, ProtocolVersion.V1, 0x00, 0x00 }); + var error = connection.DebugReceiveNextS7plusPduForTests(100, out var pdu); + + Assert.Equal(S7Consts.errIsoInvalidPDU1, error); + Assert.Null(pdu); + } + + [Fact] + public void InvalidProtocolVersionPublishesReceiveError() + { + var connection = new S7CommPlusProtocolSession(); + connection.DebugResetReceiveDispatcherForTests(); + + connection.DebugOnDataReceivedForTests(new byte[] { 0x72, 0x42, 0x00, 0x00 }); + var error = connection.DebugReceiveNextS7plusPduForTests(100, out var pdu); + + Assert.Equal(S7Consts.errIsoInvalidPDU2, error); + Assert.Null(pdu); + } + + [Fact] + public void CompletePduIsDispatchedWithoutPolling() + { + var connection = new S7CommPlusProtocolSession(); + connection.DebugResetReceiveDispatcherForTests(); + + connection.DebugOnDataReceivedForTests(new byte[] { 0x72, ProtocolVersion.V1, 0x00, 0x01, 0xAA, 0x72, ProtocolVersion.V1, 0x00, 0x00 }); + var error = connection.DebugReceiveNextS7plusPduForTests(100, out MemoryStream pdu); + + Assert.Equal(0, error); + Assert.NotNull(pdu); + Assert.Equal(new byte[] { ProtocolVersion.V1, 0xAA }, pdu.ToArray()); + } + + [Fact] + public void ReceiveTimeoutReturnsJobTimeout() + { + var connection = new S7CommPlusProtocolSession(); + connection.DebugResetReceiveDispatcherForTests(); + + var error = connection.DebugReceiveNextS7plusPduForTests(10, out var pdu); + + Assert.Equal(S7Consts.errCliJobTimeout, error); + Assert.Null(pdu); + } + + [Fact] + public void LegacyDigestMismatchPublishesReceiveError() + { + var connection = new S7CommPlusProtocolSession(); + connection.DebugResetReceiveDispatcherForTests(); + connection.DebugEnableLegacyDigestForTests(new byte[24]); + + var pdu = new byte[4 + 1 + 32 + 1 + 4]; + pdu[0] = 0x72; + pdu[1] = ProtocolVersion.V3; + pdu[3] = 34; + pdu[4] = 0x20; + pdu[37] = 0x31; + pdu[38] = 0x72; + pdu[39] = ProtocolVersion.V3; + + connection.DebugOnDataReceivedForTests(pdu); + var error = connection.DebugReceiveNextS7plusPduForTests(100, out var received); + + Assert.Equal(S7Consts.errS7CommPlusDigestMismatch, error); + Assert.Null(received); + } + + [Fact] + public void LegacyFragmentedPduUsesAccumulatedDigestInput() + { + var connection = new S7CommPlusProtocolSession(); + var sessionKey = new byte[24]; + for (var i = 0; i < sessionKey.Length; i++) + { + sessionKey[i] = (byte)(i + 1); + } + + var firstBody = new byte[] { 0x31, 0x01, 0x02 }; + var secondBody = new byte[] { 0x03, 0x04 }; + var accumulatedBody = new byte[firstBody.Length + secondBody.Length]; + firstBody.CopyTo(accumulatedBody, 0); + secondBody.CopyTo(accumulatedBody, firstBody.Length); + + connection.DebugResetReceiveDispatcherForTests(); + connection.DebugEnableLegacyDigestForTests(sessionKey); + + connection.DebugOnDataReceivedForTests(CreateLegacyFragment(firstBody, firstBody, sessionKey, hasTrailer: false)); + connection.DebugOnDataReceivedForTests(CreateLegacyFragment(secondBody, accumulatedBody, sessionKey, hasTrailer: true)); + var error = connection.DebugReceiveNextS7plusPduForTests(100, out MemoryStream pdu); + + Assert.Equal(0, error); + Assert.NotNull(pdu); + Assert.Equal(new byte[] { ProtocolVersion.V3, 0x31, 0x01, 0x02, 0x03, 0x04 }, pdu.ToArray()); + } + + [Fact] + public void LegacyFragmentedPduKeepsAccumulatingDigestInputAcrossMiddleFragments() + { + var connection = new S7CommPlusProtocolSession(); + var sessionKey = new byte[24]; + for (var i = 0; i < sessionKey.Length; i++) + { + sessionKey[i] = (byte)(0x30 + i); + } + + var firstBody = new byte[] { 0x31, 0xAA }; + var secondBody = new byte[] { 0xBB, 0xCC }; + var thirdBody = new byte[] { 0xDD, 0xEE }; + var secondDigestInput = new byte[firstBody.Length + secondBody.Length]; + firstBody.CopyTo(secondDigestInput, 0); + secondBody.CopyTo(secondDigestInput, firstBody.Length); + var thirdDigestInput = new byte[secondDigestInput.Length + thirdBody.Length]; + secondDigestInput.CopyTo(thirdDigestInput, 0); + thirdBody.CopyTo(thirdDigestInput, secondDigestInput.Length); + + connection.DebugResetReceiveDispatcherForTests(); + connection.DebugEnableLegacyDigestForTests(sessionKey); + + connection.DebugOnDataReceivedForTests(CreateLegacyFragment(firstBody, firstBody, sessionKey, hasTrailer: false)); + connection.DebugOnDataReceivedForTests(CreateLegacyFragment(secondBody, secondDigestInput, sessionKey, hasTrailer: false)); + connection.DebugOnDataReceivedForTests(CreateLegacyFragment(thirdBody, thirdDigestInput, sessionKey, hasTrailer: true)); + var error = connection.DebugReceiveNextS7plusPduForTests(100, out MemoryStream pdu); + + Assert.Equal(0, error); + Assert.NotNull(pdu); + Assert.Equal(new byte[] { ProtocolVersion.V3, 0x31, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE }, pdu.ToArray()); + } + + [Fact] + public void LegacyContinuationFragmentDigestMismatchDoesNotAbortReassembly() + { + var connection = new S7CommPlusProtocolSession(); + var sessionKey = new byte[24]; + for (var i = 0; i < sessionKey.Length; i++) + { + sessionKey[i] = (byte)(0x80 + i); + } + + var firstBody = new byte[] { 0x32, 0x10 }; + var secondBody = new byte[] { 0x20, 0x30 }; + + connection.DebugResetReceiveDispatcherForTests(); + connection.DebugEnableLegacyDigestForTests(sessionKey); + + connection.DebugOnDataReceivedForTests(CreateLegacyFragment(firstBody, firstBody, sessionKey, hasTrailer: false)); + connection.DebugOnDataReceivedForTests(CreateLegacyFragment(secondBody, secondBody, sessionKey, hasTrailer: true)); + var error = connection.DebugReceiveNextS7plusPduForTests(100, out MemoryStream pdu); + + Assert.Equal(0, error); + Assert.NotNull(pdu); + Assert.Equal(new byte[] { ProtocolVersion.V3, 0x32, 0x10, 0x20, 0x30 }, pdu.ToArray()); + } + + [Fact] + public void LargeGetMultiVariablesRequestExceedsLegacySingleFrameLimit() + { + var connection = new S7CommPlusProtocolSession(); + var addresses = Enumerable.Range(1, 100) + .Select(i => new ItemAddress($"8A0E{i:X4}.1")) + .ToArray(); + + var exceedsLimit = connection.DebugGetMultiVariablesRequestExceedsLegacySingleFrameForTests(addresses); + + Assert.True(exceedsLimit); + } + + [Fact] + public void SmallGetMultiVariablesRequestFitsLegacySingleFrameLimit() + { + var connection = new S7CommPlusProtocolSession(); + var addresses = Enumerable.Range(1, 8) + .Select(i => new ItemAddress($"8A0E{i:X4}.1")) + .ToArray(); + + var exceedsLimit = connection.DebugGetMultiVariablesRequestExceedsLegacySingleFrameForTests(addresses); + + Assert.False(exceedsLimit); + } + + [Fact] + public void OversizedLegacyPayloadIsRejectedBeforeFragmentation() + { + var connection = new S7CommPlusProtocolSession(); + connection.DebugEnableLegacyDigestForTests(new byte[24]); + + var error = connection.DebugSendLegacyPayloadForTests(new byte[2000]); + + Assert.Equal(S7Consts.errS7CommPlusLegacyRequestTooLarge, error); + } + + private static byte[] CreateLegacyFragment(byte[] body, byte[] digestInput, byte[] sessionKey, bool hasTrailer) + { + var pdu = new byte[4 + 1 + 32 + body.Length + (hasTrailer ? 4 : 0)]; + var dataLength = 1 + 32 + body.Length; + pdu[0] = 0x72; + pdu[1] = ProtocolVersion.V3; + pdu[2] = (byte)(dataLength >> 8); + pdu[3] = (byte)dataLength; + pdu[4] = 32; + HMACSHA256.HashData(sessionKey, digestInput, pdu.AsSpan(5, 32)); + body.CopyTo(pdu, 37); + if (hasTrailer) + { + pdu[^4] = 0x72; + pdu[^3] = ProtocolVersion.V3; + } + + return pdu; + } + } +} diff --git a/src/S7CommPlusDriver.Tests/S7CommPlusSymbolCrcTests.cs b/src/S7CommPlusDriver.Tests/S7CommPlusSymbolCrcTests.cs new file mode 100644 index 0000000..a54e958 --- /dev/null +++ b/src/S7CommPlusDriver.Tests/S7CommPlusSymbolCrcTests.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using S7CommPlusDriver.Internal; +using Xunit; + +namespace S7CommPlusDriver.Tests +{ + public sealed class S7CommPlusSymbolCrcTests + { + [Fact] + public void ComputesKnownScalarCrcs() + { + Assert.Equal(0x4CA70840u, S7CommPlusSymbolCrc.ComputeItemCrc("Test2", Softdatatype.S7COMMP_SOFTDATATYPE_DINT)); + Assert.Equal(0xFC8BA389u, S7CommPlusSymbolCrc.ComputeItemCrc("flag", Softdatatype.S7COMMP_SOFTDATATYPE_BOOL)); + } + + [Fact] + public void ComputesKnownArrayCrcs() + { + Assert.Equal(0x0CC865FBu, S7CommPlusSymbolCrc.ComputeArrayItemCrc("TEST 3", Softdatatype.S7COMMP_SOFTDATATYPE_BOOL, 0)); + Assert.Equal(0x6580D02Cu, S7CommPlusSymbolCrc.ComputeArrayItemCrc("readings", Softdatatype.S7COMMP_SOFTDATATYPE_REAL, 0)); + Assert.Equal(0x6E16F413u, S7CommPlusSymbolCrc.ComputeArrayItemCrc("sintItems", Softdatatype.S7COMMP_SOFTDATATYPE_SINT, 1)); + Assert.Equal(0x007CD376u, S7CommPlusSymbolCrc.ComputeArrayItemCrc("realItems", Softdatatype.S7COMMP_SOFTDATATYPE_REAL, 1)); + } + + [Fact] + public void ComputesNestedStructCrc() + { + var path = new List + { + S7CommPlusSymbolCrc.PathSegment.Member("TEST 4", Softdatatype.S7COMMP_SOFTDATATYPE_STRUCT), + S7CommPlusSymbolCrc.PathSegment.Member("T4ChildBool", Softdatatype.S7COMMP_SOFTDATATYPE_BOOL) + }; + + Assert.Equal(0x1AC12998u, S7CommPlusSymbolCrc.ComputeFromSegments(path)); + } + + [Fact] + public void BBoolUsesBoolTypeCode() + { + Assert.Equal( + S7CommPlusSymbolCrc.ComputeItemCrc("packedFlag", Softdatatype.S7COMMP_SOFTDATATYPE_BOOL), + S7CommPlusSymbolCrc.ComputeItemCrc("packedFlag", Softdatatype.S7COMMP_SOFTDATATYPE_BBOOL)); + } + + [Fact] + public void ComputesNestedArrayMemberCrc() + { + var path = new List + { + S7CommPlusSymbolCrc.PathSegment.Array("items", Softdatatype.S7COMMP_SOFTDATATYPE_STRUCT, 1), + S7CommPlusSymbolCrc.PathSegment.Member("value", Softdatatype.S7COMMP_SOFTDATATYPE_REAL) + }; + + var repeated = S7CommPlusSymbolCrc.ComputeFromSegments(path); + + Assert.NotEqual(0u, repeated); + Assert.Equal(repeated, S7CommPlusSymbolCrc.ComputeFromSegments(path)); + } + } +} diff --git a/src/S7CommPlusDriver.sln b/src/S7CommPlusDriver.sln deleted file mode 100644 index 574c9b6..0000000 --- a/src/S7CommPlusDriver.sln +++ /dev/null @@ -1,71 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.33328.57 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "S7CommPlusDriver", "S7CommPlusDriver\S7CommPlusDriver.csproj", "{781951D8-80DE-4E2F-92CA-A5FEF0059415}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DriverTest", "DriverTest\DriverTest.csproj", "{94755B6A-7E0D-40F9-A2ED-48FF0C0FEA2B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "S7CommPlusGUIBrowser", "S7CommPlusGUIBrowser\S7CommPlusGUIBrowser.csproj", "{BEB37276-B3B3-4DEE-A1EA-D95EB774223B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zlib.net", "Zlib.net\Zlib.net.csproj", "{0C89B7A2-A218-49E4-B545-5B044A45F977}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {781951D8-80DE-4E2F-92CA-A5FEF0059415}.Debug|Any CPU.ActiveCfg = Debug|x86 - {781951D8-80DE-4E2F-92CA-A5FEF0059415}.Debug|x64.ActiveCfg = Debug|x64 - {781951D8-80DE-4E2F-92CA-A5FEF0059415}.Debug|x64.Build.0 = Debug|x64 - {781951D8-80DE-4E2F-92CA-A5FEF0059415}.Debug|x86.ActiveCfg = Debug|x86 - {781951D8-80DE-4E2F-92CA-A5FEF0059415}.Debug|x86.Build.0 = Debug|x86 - {781951D8-80DE-4E2F-92CA-A5FEF0059415}.Release|Any CPU.ActiveCfg = Release|x86 - {781951D8-80DE-4E2F-92CA-A5FEF0059415}.Release|x64.ActiveCfg = Release|x64 - {781951D8-80DE-4E2F-92CA-A5FEF0059415}.Release|x64.Build.0 = Release|x64 - {781951D8-80DE-4E2F-92CA-A5FEF0059415}.Release|x86.ActiveCfg = Release|x86 - {781951D8-80DE-4E2F-92CA-A5FEF0059415}.Release|x86.Build.0 = Release|x86 - {94755B6A-7E0D-40F9-A2ED-48FF0C0FEA2B}.Debug|Any CPU.ActiveCfg = Debug|x86 - {94755B6A-7E0D-40F9-A2ED-48FF0C0FEA2B}.Debug|x64.ActiveCfg = Debug|x64 - {94755B6A-7E0D-40F9-A2ED-48FF0C0FEA2B}.Debug|x64.Build.0 = Debug|x64 - {94755B6A-7E0D-40F9-A2ED-48FF0C0FEA2B}.Debug|x86.ActiveCfg = Debug|x86 - {94755B6A-7E0D-40F9-A2ED-48FF0C0FEA2B}.Debug|x86.Build.0 = Debug|x86 - {94755B6A-7E0D-40F9-A2ED-48FF0C0FEA2B}.Release|Any CPU.ActiveCfg = Release|x86 - {94755B6A-7E0D-40F9-A2ED-48FF0C0FEA2B}.Release|x64.ActiveCfg = Release|x64 - {94755B6A-7E0D-40F9-A2ED-48FF0C0FEA2B}.Release|x64.Build.0 = Release|x64 - {94755B6A-7E0D-40F9-A2ED-48FF0C0FEA2B}.Release|x86.ActiveCfg = Release|x86 - {94755B6A-7E0D-40F9-A2ED-48FF0C0FEA2B}.Release|x86.Build.0 = Release|x86 - {BEB37276-B3B3-4DEE-A1EA-D95EB774223B}.Debug|Any CPU.ActiveCfg = Debug|x86 - {BEB37276-B3B3-4DEE-A1EA-D95EB774223B}.Debug|x64.ActiveCfg = Debug|x64 - {BEB37276-B3B3-4DEE-A1EA-D95EB774223B}.Debug|x64.Build.0 = Debug|x64 - {BEB37276-B3B3-4DEE-A1EA-D95EB774223B}.Debug|x86.ActiveCfg = Debug|x86 - {BEB37276-B3B3-4DEE-A1EA-D95EB774223B}.Debug|x86.Build.0 = Debug|x86 - {BEB37276-B3B3-4DEE-A1EA-D95EB774223B}.Release|Any CPU.ActiveCfg = Release|x86 - {BEB37276-B3B3-4DEE-A1EA-D95EB774223B}.Release|x64.ActiveCfg = Release|x64 - {BEB37276-B3B3-4DEE-A1EA-D95EB774223B}.Release|x64.Build.0 = Release|x64 - {BEB37276-B3B3-4DEE-A1EA-D95EB774223B}.Release|x86.ActiveCfg = Release|x86 - {BEB37276-B3B3-4DEE-A1EA-D95EB774223B}.Release|x86.Build.0 = Release|x86 - {0C89B7A2-A218-49E4-B545-5B044A45F977}.Debug|Any CPU.ActiveCfg = Debug|x86 - {0C89B7A2-A218-49E4-B545-5B044A45F977}.Debug|x64.ActiveCfg = Debug|x64 - {0C89B7A2-A218-49E4-B545-5B044A45F977}.Debug|x64.Build.0 = Debug|x64 - {0C89B7A2-A218-49E4-B545-5B044A45F977}.Debug|x86.ActiveCfg = Debug|x86 - {0C89B7A2-A218-49E4-B545-5B044A45F977}.Debug|x86.Build.0 = Debug|x86 - {0C89B7A2-A218-49E4-B545-5B044A45F977}.Release|Any CPU.ActiveCfg = Release|x86 - {0C89B7A2-A218-49E4-B545-5B044A45F977}.Release|x64.ActiveCfg = Release|x64 - {0C89B7A2-A218-49E4-B545-5B044A45F977}.Release|x64.Build.0 = Release|x64 - {0C89B7A2-A218-49E4-B545-5B044A45F977}.Release|x86.ActiveCfg = Release|x86 - {0C89B7A2-A218-49E4-B545-5B044A45F977}.Release|x86.Build.0 = Release|x86 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {641BA78D-D608-4377-9F99-E4C1029EFA0B} - EndGlobalSection -EndGlobal diff --git a/src/S7CommPlusDriver.slnx b/src/S7CommPlusDriver.slnx new file mode 100644 index 0000000..c0e6e5b --- /dev/null +++ b/src/S7CommPlusDriver.slnx @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/S7CommPlusDriver/Alarming/AlarmsAlarmTexts.cs b/src/S7CommPlusDriver/Alarming/AlarmsAlarmTexts.cs deleted file mode 100644 index 1ba08c6..0000000 --- a/src/S7CommPlusDriver/Alarming/AlarmsAlarmTexts.cs +++ /dev/null @@ -1,118 +0,0 @@ -#region License -/****************************************************************************** - * S7CommPlusDriver - * - * Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de - * - * This file is part of S7CommPlusDriver. - * - * S7CommPlusDriver is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - /****************************************************************************/ -#endregion - -using System; -using System.Collections.Generic; - -namespace S7CommPlusDriver.Alarming -{ - public class AlarmsAlarmTexts - { - public int LanguageId; - public string Infotext = String.Empty; - public string AlarmText = String.Empty; - public string AdditionalText1 = String.Empty; - public string AdditionalText2 = String.Empty; - public string AdditionalText3 = String.Empty; - public string AdditionalText4 = String.Empty; - public string AdditionalText5 = String.Empty; - public string AdditionalText6 = String.Empty; - public string AdditionalText7 = String.Empty; - public string AdditionalText8 = String.Empty; - public string AdditionalText9 = String.Empty; - - // These two values we get in addition when browsing for the alarmtexts - // Don't know if they are useful for something. - public ushort UnknownValue1; - public ushort UnknownValue2; - - public static AlarmsAlarmTexts FromNotificationBlob(ValueBlobSparseArray blob, int languageId) - { - var at = new AlarmsAlarmTexts(); - string s; - int lcid; - int textid; - at.LanguageId = languageId; - foreach (var v in blob.Value) - { - s = Utils.GetUtfString(v.Value.value, 0, (uint)v.Value.value.Length); - // Values in older CPUs, from: 0xa09c8001..0xa09c800b (2694610945..2694610955) - // Current CPUs use: 0x04070001..0x0407000b ( 67567617.. 67567627) - // Where the left word is the language ID, 0x0407 = 1031, and the right word is the text id. - // The blob may contain several languages. If you need them all, you need to call this multiple times. - lcid = (int)(v.Key >> 16); - textid = (int)(v.Key & 0xffff); - if (lcid == languageId) - { - switch (textid) - { - case 1: - at.Infotext = s; - break; - case 2: - at.AlarmText = s; - break; - case 3: - at.AdditionalText1 = s; - break; - case 4: - at.AdditionalText2 = s; - break; - case 5: - at.AdditionalText3 = s; - break; - case 6: - at.AdditionalText4 = s; - break; - case 7: - at.AdditionalText5 = s; - break; - case 8: - at.AdditionalText6 = s; - break; - case 9: - at.AdditionalText7 = s; - break; - case 10: - at.AdditionalText8 = s; - break; - case 11: - at.AdditionalText9 = s; - break; - } - } - } - return at; - } - - public override string ToString() - { - string s = "" + Environment.NewLine; - s += "" + Infotext.ToString() + "" + Environment.NewLine; - s += "" + AlarmText.ToString() + "" + Environment.NewLine; - s += "" + AdditionalText1.ToString() + "" + Environment.NewLine; - s += "" + AdditionalText2.ToString() + "" + Environment.NewLine; - s += "" + AdditionalText3.ToString() + "" + Environment.NewLine; - s += "" + AdditionalText4.ToString() + "" + Environment.NewLine; - s += "" + AdditionalText5.ToString() + "" + Environment.NewLine; - s += "" + AdditionalText6.ToString() + "" + Environment.NewLine; - s += "" + AdditionalText7.ToString() + "" + Environment.NewLine; - s += "" + AdditionalText8.ToString() + "" + Environment.NewLine; - s += "" + AdditionalText9.ToString() + "" + Environment.NewLine; - s += "" + Environment.NewLine; - return s; - } - } -} diff --git a/src/S7CommPlusDriver/Alarming/AlarmsAssociatedValues.cs b/src/S7CommPlusDriver/Alarming/AlarmsAssociatedValues.cs deleted file mode 100644 index 1a4f97c..0000000 --- a/src/S7CommPlusDriver/Alarming/AlarmsAssociatedValues.cs +++ /dev/null @@ -1,282 +0,0 @@ -#region License -/****************************************************************************** - * S7CommPlusDriver - * - * Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de - * - * This file is part of S7CommPlusDriver. - * - * S7CommPlusDriver is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - /****************************************************************************/ -#endregion - -using System; -using System.Text; - -namespace S7CommPlusDriver.Alarming -{ - public class AlarmsAssociatedValues - { - public AssociatedValue SD_1; - public AssociatedValue SD_2; - public AssociatedValue SD_3; - public AssociatedValue SD_4; - public AssociatedValue SD_5; - public AssociatedValue SD_6; - public AssociatedValue SD_7; - public AssociatedValue SD_8; - public AssociatedValue SD_9; - public AssociatedValue SD_10; - - public override string ToString() - { - string s = "" + Environment.NewLine; - s += "" + (SD_1 is null ? String.Empty : SD_1.ToString()) + "" + Environment.NewLine; - s += "" + (SD_2 is null ? String.Empty : SD_2.ToString()) + "" + Environment.NewLine; - s += "" + (SD_3 is null ? String.Empty : SD_3.ToString()) + "" + Environment.NewLine; - s += "" + (SD_4 is null ? String.Empty : SD_4.ToString()) + "" + Environment.NewLine; - s += "" + (SD_5 is null ? String.Empty : SD_5.ToString()) + "" + Environment.NewLine; - s += "" + (SD_6 is null ? String.Empty : SD_6.ToString()) + "" + Environment.NewLine; - s += "" + (SD_7 is null ? String.Empty : SD_7.ToString()) + "" + Environment.NewLine; - s += "" + (SD_8 is null ? String.Empty : SD_8.ToString()) + "" + Environment.NewLine; - s += "" + (SD_9 is null ? String.Empty : SD_9.ToString()) + "" + Environment.NewLine; - s += "" + (SD_10 is null ? String.Empty : SD_10.ToString()) + "" + Environment.NewLine; - s += "" + Environment.NewLine; - return s; - } - - public static AlarmsAssociatedValues FromValueBlob(ValueBlobArray blob) - { - var av = new AlarmsAssociatedValues(); - var blobs = blob.GetValue(); - // Comes as Array[17], with indices: - // 0 = Unknown Typeinformation, 4 Bytes - // 1..10 = SD_1..SD_10 - // - // The typeinformation at index 0 has a BlobRootId of 3476 = AS_CGS.AssociatedValues - // When browsing 0x2000113 we get the result: - // Type Name - // --------------- - // UInt Syntax - // Byte Aap - int i = 0; - AssociatedValue pv; - foreach(var b in blobs) - { - var bytes = b.GetValue(); - switch (b.BlobRootId) - { - case (Ids.TI_BOOL): - pv = new AssociatedValue(b.BlobRootId); - pv.SetBool(bytes[0] != 0); - av.SetSDValue(pv, i); - break; - case (Ids.TI_BYTE): - pv = new AssociatedValue(b.BlobRootId); - pv.SetInt(bytes[0]); - av.SetSDValue(pv, i); - break; - case (Ids.TI_CHAR): - pv = new AssociatedValue(b.BlobRootId); - pv.SetString(Encoding.GetEncoding("ISO-8859-1").GetString(bytes, 0, 1)); - av.SetSDValue(pv, i); - break; - case (Ids.TI_WORD): - pv = new AssociatedValue(b.BlobRootId); - pv.SetInt(Utils.GetUInt16(bytes, 0)); - av.SetSDValue(pv, i); - break; - case (Ids.TI_INT): - pv = new AssociatedValue(b.BlobRootId); - pv.SetInt(Utils.GetInt16(bytes, 0)); - av.SetSDValue(pv, i); - break; - case (Ids.TI_DWORD): - pv = new AssociatedValue(b.BlobRootId); - pv.SetInt(Utils.GetUInt32(bytes, 0)); - av.SetSDValue(pv, i); - break; - case (Ids.TI_DINT): - pv = new AssociatedValue(b.BlobRootId); - pv.SetInt(Utils.GetInt32(bytes, 0)); - av.SetSDValue(pv, i); - break; - case (Ids.TI_REAL): - pv = new AssociatedValue(b.BlobRootId); - pv.SetReal(Utils.GetFloat(bytes, 0)); - av.SetSDValue(pv, i); - break; - case (Ids.TI_LREAL): - pv = new AssociatedValue(b.BlobRootId); - pv.SetReal(Utils.GetDouble(bytes, 0)); - av.SetSDValue(pv, i); - break; - case (Ids.TI_USINT): - pv = new AssociatedValue(b.BlobRootId); - pv.SetInt(bytes[0]); - av.SetSDValue(pv, i); - break; - case (Ids.TI_UINT): - pv = new AssociatedValue(b.BlobRootId); - pv.SetInt(Utils.GetUInt16(bytes, 0)); - av.SetSDValue(pv, i); - break; - case (Ids.TI_UDINT): - pv = new AssociatedValue(b.BlobRootId); - pv.SetInt(Utils.GetUInt32(bytes, 0)); - av.SetSDValue(pv, i); - break; - case (Ids.TI_SINT): - pv = new AssociatedValue(b.BlobRootId); - pv.SetInt((sbyte)bytes[0]); - av.SetSDValue(pv, i); - break; - case (Ids.TI_WCHAR): - pv = new AssociatedValue(b.BlobRootId); - pv.SetString(((char)Utils.GetUInt16(bytes, 0)).ToString()); - av.SetSDValue(pv, i); - break; - default: - if (b.BlobRootId > Ids.TI_STRING_START && b.BlobRootId <= Ids.TI_STRING_END) - { - //byte s_maxlen = bytes[0]; // Don't need this value - byte s_actlen = bytes[1]; - pv = new AssociatedValue(Ids.TI_STRING); - pv.SetString(Encoding.GetEncoding("ISO-8859-1").GetString(bytes, 2, s_actlen)); - av.SetSDValue(pv, i); - } - else if (b.BlobRootId > Ids.TI_WSTRING_START && b.BlobRootId <= Ids.TI_WSTRING_END) - { - //int ws_maxlen = Utils.GetUInt16(bytes, 0); // Don't need this value - int ws_actlen = Utils.GetUInt16(bytes, 2); - pv = new AssociatedValue(Ids.TI_WSTRING); - pv.SetString(Encoding.BigEndianUnicode.GetString(bytes, 4, ws_actlen * 2)); - av.SetSDValue(pv, i); - } - break; - } - i++; - // All other elements have no value - if (i > 10) - { - break; - } - } - return av; - } - - private void SetSDValue(AssociatedValue v, int index) - { - switch(index) - { - case 1: SD_1 = v; break; - case 2: SD_2 = v; break; - case 3: SD_3 = v; break; - case 4: SD_4 = v; break; - case 5: SD_5 = v; break; - case 6: SD_6 = v; break; - case 7: SD_7 = v; break; - case 8: SD_8 = v; break; - case 9: SD_9 = v; break; - case 10: SD_10 = v; break; - } - } - } - - public class AssociatedValue - { - bool ValueBool; - Int64 ValueInt; - double ValueReal; - string ValueString; - - public uint TypeInfo; - // Allowed types in plc program: Bool, Byte, Char, DInt, DWord, Int, LReal, Real, SInt, String, UDInt, UInt, WChar, Word, WString - // Break down to .Net types which can handle all these values: Bool, Int64, double, string - - public AssociatedValue(uint typeinfo) - { - TypeInfo = typeinfo; - } - - public void SetBool(bool value) - { - ValueBool = value; - } - - public void SetInt(Int64 value) - { - ValueInt = value; - } - - public void SetReal(double value) - { - ValueReal = value; - } - - public void SetString(string value) - { - ValueString = value; - } - - public override string ToString() - { - string s = String.Empty; - switch (TypeInfo) - { - case (Ids.TI_BOOL): - s = ValueBool.ToString(); - break; - case (Ids.TI_BYTE): - s = ValueInt.ToString(); - break; - case (Ids.TI_CHAR): - s = ValueString; - break; - case (Ids.TI_WORD): - s = ValueInt.ToString(); - break; - case (Ids.TI_INT): - s = ValueInt.ToString(); - break; - case (Ids.TI_DWORD): - s = ValueInt.ToString(); - break; - case (Ids.TI_DINT): - s = ValueInt.ToString(); - break; - case (Ids.TI_REAL): - s = ValueReal.ToString(); - break; - case (Ids.TI_LREAL): - s = ValueReal.ToString(); - break; - case (Ids.TI_USINT): - s = ValueInt.ToString(); - break; - case (Ids.TI_UINT): - s = ValueInt.ToString(); - break; - case (Ids.TI_UDINT): - s = ValueInt.ToString(); - break; - case (Ids.TI_SINT): - s = ValueInt.ToString(); - break; - case (Ids.TI_WCHAR): - s = ValueString; - break; - case (Ids.TI_STRING): - s = ValueString; - break; - case (Ids.TI_WSTRING): - s = ValueString; - break; - } - return s; - } - } -} diff --git a/src/S7CommPlusDriver/Alarming/AlarmsHandler.cs b/src/S7CommPlusDriver/Alarming/AlarmsHandler.cs deleted file mode 100644 index abbb692..0000000 --- a/src/S7CommPlusDriver/Alarming/AlarmsHandler.cs +++ /dev/null @@ -1,174 +0,0 @@ -#region License -/****************************************************************************** - * S7CommPlusDriver - * - * Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de - * - * This file is part of S7CommPlusDriver. - * - * S7CommPlusDriver is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - /****************************************************************************/ -#endregion - -using System; -using S7CommPlusDriver.Alarming; - -namespace S7CommPlusDriver -{ - public partial class S7CommPlusConnection - { - // ************************************************* - // * IMPORTANT! * - // * This is basically a test for Alarming, * - // * how to use it, and how to later integrate * - // * this into the complete library! * - // ************************************************* - // - // Example code for testing: - // CultureInfo ci = new CultureInfo("en-US"); - // conn.AlarmSubscriptionCreate(); - // conn.TestWaitForAlarmNotifications(20000, 3, ci.LCID); - // conn.AlarmSubscriptionDelete(); - - uint m_AlarmSubscriptionRelationId = 0x7fffc001; // TODO! Unknown value! See also Subscription.cs - uint m_AlarmSubscriptionRefRelationId = 0x51010001; // TODO! Unknown value! - short m_AlarmNextCreditLimit; - uint m_AlarmSubscriptionObjectId; - - public int AlarmSubscriptionCreate() - { - int res; - PObject subsobj = new PObject(); - subsobj.ClassId = Ids.ClassSubscription; - subsobj.RelationId = m_AlarmSubscriptionRelationId; - subsobj.AddAttribute(Ids.ObjectVariableTypeName, new ValueWString("Subscription_" + m_AlarmSubscriptionRelationId.ToString())); - subsobj.AddAttribute(Ids.SubscriptionFunctionClassId, new ValueUSInt(2)); - subsobj.AddAttribute(Ids.SubscriptionMissedSendings, new ValueUInt(0)); - subsobj.AddAttribute(Ids.SubscriptionSubsystemError, new ValueLInt(0)); - subsobj.AddAttribute(Ids.SubscriptionRouteMode, new ValueUSInt(2)); // TODO Unknown - subsobj.AddAttribute(Ids.SubscriptionActive, new ValueBool(true)); - subsobj.AddAttribute(Ids.SubscriptionReferenceList, new ValueUDIntArray(new uint[3] { 0x80010000, 0, 0 }, 0x20)); // 0x20 = Adressarray - subsobj.AddAttribute(Ids.SubscriptionCycleTime, new ValueUDInt(0)); - subsobj.AddAttribute(Ids.SubscriptionDelayTime, new ValueUDInt(0)); - subsobj.AddAttribute(Ids.SubscriptionDisabled, new ValueUSInt(0)); - subsobj.AddAttribute(Ids.SubscriptionCount, new ValueUSInt(0)); - m_AlarmNextCreditLimit = 10; - subsobj.AddAttribute(Ids.SubscriptionCreditLimit, new ValueInt(m_AlarmNextCreditLimit)); // -1=unlimited, 255 = max - subsobj.AddAttribute(Ids.SubscriptionTicks, new ValueUInt(65535)); // 65535 - // 1055 = Unknown -> is working without setting this. Maybe default attribute is zero. - //subsobj.AddAttribute(1055, new ValueUSInt(0)); - PObject asrefsobj = new PObject(); - asrefsobj.ClassId = Ids.AlarmSubscriptionRef_Class_Rid; - asrefsobj.RelationId = m_AlarmSubscriptionRefRelationId; - asrefsobj.AddAttribute(Ids.ObjectVariableTypeName, new ValueWString("S7pDriver_Alarming")); - asrefsobj.AddAttribute(Ids.SubscriptionReferenceTriggerAndTransmitMode, new ValueUSInt(3)); - asrefsobj.AddAttribute(Ids.AlarmSubscriptionRef_AlarmDomain, new ValueUIntArray(new ushort[10] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 0x10)); - // Also variant to set explicit the alarm domain as filter, for example: - // {1, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272} - // Possibly 65535 is "all". - asrefsobj.AddAttribute(Ids.AlarmSubscriptionRef_AlarmDomain2, new ValueUIntArray(new ushort[1] { 65535 }, 0x20)); // 0x20 = Adressarray - // OPTION: - // Send text informations with the message, we don't need to browse them in advance. - asrefsobj.AddAttribute(Ids.AlarmSubscriptionRef_AlarmTextLanguages_Rid, new ValueUDIntArray(new uint[0], 0x20)); // Empty for all languanges? Otherwise e.g. 1031 for de-de or what you need. - asrefsobj.AddAttribute(Ids.AlarmSubscriptionRef_SendAlarmTexts_Rid, new ValueBool(true)); - - asrefsobj.AddRelation(Ids.AlarmSubscriptionRef_itsAlarmSubsystem, 0x00000008); - subsobj.AddObject(asrefsobj); - // Build the request object - var createObjReq = new CreateObjectRequest(ProtocolVersion.V2, 0, true); - createObjReq.TransportFlags = 0x34; - createObjReq.RequestId = SessionId2; - createObjReq.RequestValue = new ValueUDInt(0); - createObjReq.SetRequestObject(subsobj); - - // Send it - res = SendS7plusFunctionObject(createObjReq); - if (res != 0) - { - m_client.Disconnect(); - return res; - } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - m_client.Disconnect(); - return m_LastError; - } - - var createObjRes = CreateObjectResponse.DeserializeFromPdu(m_ReceivedPDU); - if (createObjRes == null) - { - Console.WriteLine("AlarmSubscription - Create: CreateObjectResponse with Error!"); - return S7Consts.errIsoInvalidPDU; - } - - if (createObjRes.ReturnValue == 0) - { - // Save the ObjectId, to modify the existing subscription - m_AlarmSubscriptionObjectId = createObjRes.ObjectIds[0]; - } - else - { - // If creating a subscription fails, the object is still created and should be deleted. - // At least deleting it, gives no error. - Console.WriteLine(String.Format("AlarmSubscription - Create: Failed with Returnvalue = 0x{0:X8}", createObjRes.ReturnValue)); - res = S7Consts.errCliInvalidParams; - } - - return res; - } - - public int TestWaitForAlarmNotifications(int waitTimeout, int untilNumberOfAlarms, int alarmTextsLanguageId) - { - int res = 0; - short creditLimitStep = 5; - - for (int i = 1; i <= untilNumberOfAlarms; i++) - { - Console.WriteLine(Environment.NewLine + "WaitForAlarmNotifications(): *** Loop #" + i.ToString() + " ***"); - m_LastError = 0; - WaitForNewS7plusReceived(waitTimeout); - if (m_LastError != 0) - { - return m_LastError; - } - - var noti = Notification.DeserializeFromPdu(m_ReceivedPDU); - if (noti == null) - { - Console.WriteLine("Notification == null!"); - return S7Consts.errIsoInvalidPDU; - } - else - { - Console.Write("Notification: CreditTick=" + noti.NotificationCreditTick + " SequenceNumber=" + noti.NotificationSequenceNumber); - Console.WriteLine(String.Format(" PLC-Timestamp={0}.{1:D03}", noti.Add1Timestamp.ToString(), noti.Add1Timestamp.Millisecond)); - - var dai = AlarmsDai.FromNotificationObject(noti.P2Objects[0], alarmTextsLanguageId); - Console.WriteLine(dai.ToString()); - if (noti.NotificationCreditTick >= m_AlarmNextCreditLimit - 1) // Set new limit one tick before it expires, to get a constant flow of data - { - // CreditTick in Notification is only one byte - m_AlarmNextCreditLimit = (short)((m_AlarmNextCreditLimit + creditLimitStep) % 255); - Console.WriteLine("--> Credit limit of " + noti.NotificationCreditTick + " reached. SetCreditLimit to " + m_AlarmNextCreditLimit.ToString()); - SubscriptionSetCreditLimit(m_AlarmNextCreditLimit); - } - } - } - return res; - } - - public int AlarmSubscriptionDelete() - { - int res; - m_AlarmSubscriptionObjectId = 0; - Console.WriteLine(String.Format("SubscriptionDelete: Calling DeleteObject for SessionId2={0:X8}", SessionId2)); - res = DeleteObject(SessionId2); - return res; - } - } -} diff --git a/src/S7CommPlusDriver/Alarming/AlarmsDai.cs b/src/S7CommPlusDriver/Alarming/S7CommPlusAlarm.cs similarity index 52% rename from src/S7CommPlusDriver/Alarming/AlarmsDai.cs rename to src/S7CommPlusDriver/Alarming/S7CommPlusAlarm.cs index 8025a26..d8fde82 100644 --- a/src/S7CommPlusDriver/Alarming/AlarmsDai.cs +++ b/src/S7CommPlusDriver/Alarming/S7CommPlusAlarm.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -14,32 +14,42 @@ #endregion using System; +using System.Collections.Generic; namespace S7CommPlusDriver.Alarming { - public class AlarmsDai + public class S7CommPlusAlarm { public string ObjectVariableTypeName; public ulong CpuAlarmId; + public uint SourceRelationId => (uint)(CpuAlarmId >> 32); + public ushort SourceAlarmId => (ushort)((CpuAlarmId >> 16) & 0xffff); public byte AllStatesInfo; public ushort AlarmDomain; public int MessageType; public uint SequenceCounter; - public AlarmsAlarmTexts AlarmTexts; - public AlarmsHmiInfo HmiInfo; - public AlarmsAsCgs AsCgs; + public S7CommPlusAlarmTexts AlarmTexts; + /// + /// Alarm texts keyed by LCID when the PLC returned more than one language. + /// + public IReadOnlyDictionary AlarmTextsByLanguage { get; internal set; } = new Dictionary(); + public S7CommPlusAlarmHmiInfo HmiInfo; + public S7CommPlusAlarmStateChange StateChange; public override string ToString() { - string s = "" + Environment.NewLine; + string s = "" + Environment.NewLine; s += "" + ObjectVariableTypeName.ToString() + "" + Environment.NewLine; s += "" + CpuAlarmId.ToString() + "" + Environment.NewLine; + s += "" + SourceRelationId.ToString() + "" + Environment.NewLine; + s += "" + SourceAlarmId.ToString() + "" + Environment.NewLine; s += "" + AllStatesInfo.ToString() + "" + Environment.NewLine; s += "" + AlarmDomain.ToString() + "" + Environment.NewLine; s += "" + MessageType.ToString() + "" + Environment.NewLine; + s += "" + SiemensOmsEnumNames.AlarmMessageTypeName(MessageType) + "" + Environment.NewLine; s += "" + Environment.NewLine + HmiInfo.ToString() + "" + Environment.NewLine; - s += "" + Environment.NewLine + AsCgs.ToString() + "" + Environment.NewLine; + s += "" + Environment.NewLine + StateChange.ToString() + "" + Environment.NewLine; s += "" + SequenceCounter.ToString() + "" + Environment.NewLine; if (AlarmTexts != null) { @@ -49,20 +59,25 @@ public override string ToString() { s += "" + Environment.NewLine; } - s += "" + Environment.NewLine; + s += "" + Environment.NewLine; return s; } - public static AlarmsDai FromNotificationObject(PObject pobj, int alarmtextsLanguageId) + internal static S7CommPlusAlarm FromNotificationObject(PObject pobj, int alarmtextsLanguageId) { - var dai = new AlarmsDai(); + return FromNotificationObject(pobj, alarmtextsLanguageId, null); + } + + internal static S7CommPlusAlarm FromNotificationObject(PObject pobj, int alarmtextsLanguageId, Func textListResolver) + { + var dai = new S7CommPlusAlarm(); dai.ObjectVariableTypeName = ((ValueWString)pobj.GetAttribute(Ids.ObjectVariableTypeName)).GetValue(); dai.CpuAlarmId = ((ValueLWord)pobj.GetAttribute(Ids.DAI_CPUAlarmID)).GetValue(); dai.AllStatesInfo = ((ValueUSInt)pobj.GetAttribute(Ids.DAI_AllStatesInfo)).GetValue(); dai.AlarmDomain = ((ValueUInt)pobj.GetAttribute(Ids.DAI_AlarmDomain)).GetValue(); dai.MessageType = ((ValueDInt)pobj.GetAttribute(Ids.DAI_MessageType)).GetValue(); - dai.HmiInfo = AlarmsHmiInfo.FromValueBlob(((ValueBlob)pobj.GetAttribute(Ids.DAI_HmiInfo))); - // TODO: Blob for additional values + dai.HmiInfo = S7CommPlusAlarmHmiInfo.FromValueBlob(((ValueBlob)pobj.GetAttribute(Ids.DAI_HmiInfo))); + // Additional-value blobs are protocol metadata for alarm formatting; keep the raw attribute available on pobj. dai.SequenceCounter = ((ValueUDInt)pobj.GetAttribute(Ids.DAI_SequenceCounter)).GetValue(); ValueStruct str = null; uint dai_id = 0; @@ -80,10 +95,25 @@ public static AlarmsDai FromNotificationObject(PObject pobj, int alarmtextsLangu { return null; } - dai.AsCgs = AlarmsAsCgs.FromValueStruct(str); - dai.AsCgs.SubtypeId = dai_id; - dai.AlarmTexts = AlarmsAlarmTexts.FromNotificationBlob(((ValueBlobSparseArray)pobj.GetAttribute(Ids.DAI_AlarmTexts_Rid)), alarmtextsLanguageId); + dai.StateChange = S7CommPlusAlarmStateChange.FromValueStruct(str); + dai.StateChange.SubtypeId = dai_id; + var alarmTextsByLanguage = S7CommPlusAlarmTexts.FromNotificationBlobAllLanguages((ValueBlobSparseArray)pobj.GetAttribute(Ids.DAI_AlarmTexts_Rid)); + foreach (var alarmTexts in alarmTextsByLanguage.Values) + { + alarmTexts.ApplyAssociatedValues(dai.StateChange.AssociatedValues, textListResolver); + } + + dai.AlarmTextsByLanguage = alarmTextsByLanguage; + if (alarmtextsLanguageId == 0) + { + dai.AlarmTexts = S7CommPlusAlarmTexts.FirstOrEmpty(alarmTextsByLanguage); + } + else if (!alarmTextsByLanguage.TryGetValue(alarmtextsLanguageId, out dai.AlarmTexts)) + { + dai.AlarmTexts = new S7CommPlusAlarmTexts { LanguageId = alarmtextsLanguageId }; + } + return dai; } } -} \ No newline at end of file +} diff --git a/src/S7CommPlusDriver/Alarming/S7CommPlusAlarmAssociatedValues.cs b/src/S7CommPlusDriver/Alarming/S7CommPlusAlarmAssociatedValues.cs new file mode 100644 index 0000000..6571574 --- /dev/null +++ b/src/S7CommPlusDriver/Alarming/S7CommPlusAlarmAssociatedValues.cs @@ -0,0 +1,527 @@ +#region License +/****************************************************************************** + * S7CommPlusDriver + * + * Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de + * + * This file is part of S7CommPlusDriver. + * + * S7CommPlusDriver is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + /****************************************************************************/ +#endregion + +using System; +using System.Globalization; +using System.Text; + +namespace S7CommPlusDriver.Alarming +{ + public class S7CommPlusAlarmAssociatedValues + { + public S7CommPlusAlarmAssociatedValue SD_1; + public S7CommPlusAlarmAssociatedValue SD_2; + public S7CommPlusAlarmAssociatedValue SD_3; + public S7CommPlusAlarmAssociatedValue SD_4; + public S7CommPlusAlarmAssociatedValue SD_5; + public S7CommPlusAlarmAssociatedValue SD_6; + public S7CommPlusAlarmAssociatedValue SD_7; + public S7CommPlusAlarmAssociatedValue SD_8; + public S7CommPlusAlarmAssociatedValue SD_9; + public S7CommPlusAlarmAssociatedValue SD_10; + + private byte[] PackedStandardAssociatedValues; + + public override string ToString() + { + string s = "" + Environment.NewLine; + s += "" + (SD_1 is null ? String.Empty : SD_1.ToString()) + "" + Environment.NewLine; + s += "" + (SD_2 is null ? String.Empty : SD_2.ToString()) + "" + Environment.NewLine; + s += "" + (SD_3 is null ? String.Empty : SD_3.ToString()) + "" + Environment.NewLine; + s += "" + (SD_4 is null ? String.Empty : SD_4.ToString()) + "" + Environment.NewLine; + s += "" + (SD_5 is null ? String.Empty : SD_5.ToString()) + "" + Environment.NewLine; + s += "" + (SD_6 is null ? String.Empty : SD_6.ToString()) + "" + Environment.NewLine; + s += "" + (SD_7 is null ? String.Empty : SD_7.ToString()) + "" + Environment.NewLine; + s += "" + (SD_8 is null ? String.Empty : SD_8.ToString()) + "" + Environment.NewLine; + s += "" + (SD_9 is null ? String.Empty : SD_9.ToString()) + "" + Environment.NewLine; + s += "" + (SD_10 is null ? String.Empty : SD_10.ToString()) + "" + Environment.NewLine; + s += "" + Environment.NewLine; + return s; + } + + public S7CommPlusAlarmAssociatedValue GetValue(int sdIndex) + { + switch(sdIndex) + { + case 1: return SD_1; + case 2: return SD_2; + case 3: return SD_3; + case 4: return SD_4; + case 5: return SD_5; + case 6: return SD_6; + case 7: return SD_7; + case 8: return SD_8; + case 9: return SD_9; + case 10: return SD_10; + default: return null; + } + } + + internal bool TryGetPackedStandardInteger(int position, char elementType, out int value) + { + value = 0; + if (PackedStandardAssociatedValues == null || position <= 0) + { + return false; + } + + var elementTypeLength = GetElementTypeLength(elementType); + if (elementTypeLength <= 0) + { + return false; + } + + var end = (position * elementTypeLength) - 1; + var start = end - elementTypeLength + 1; + if (end >= PackedStandardAssociatedValues.Length) + { + return false; + } + + for (var i = start; i <= end; i++) + { + value = (value << 8) | PackedStandardAssociatedValues[i]; + } + return true; + } + + internal bool TryGetPackedStandardReal(int position, char elementType, out double value) + { + value = 0.0; + if (PackedStandardAssociatedValues == null || position <= 0) + { + return false; + } + + var elementTypeLength = GetElementTypeLength(elementType); + if (elementTypeLength != 4 && elementTypeLength != 8) + { + return false; + } + + var end = (position * elementTypeLength) - 1; + var start = end - elementTypeLength + 1; + if (end >= PackedStandardAssociatedValues.Length) + { + return false; + } + + value = elementTypeLength == 8 + ? Utils.GetDouble(PackedStandardAssociatedValues, (uint)start) + : Utils.GetFloat(PackedStandardAssociatedValues, (uint)start); + return true; + } + + internal static S7CommPlusAlarmAssociatedValues FromValueBlob(ValueBlobArray blob) + { + var av = new S7CommPlusAlarmAssociatedValues(); + var blobs = blob.GetValue(); + av.TrySetPackedStandardAssociatedValues(blobs); + + // Comes as Array[17], with indices: + // 0 = Unknown Typeinformation, 4 Bytes + // 1..10 = SD_1..SD_10 + // + // The typeinformation at index 0 has a BlobRootId of 3476 = AS_CGS.AssociatedValues + // When browsing 0x2000113 we get the result: + // Type Name + // --------------- + // UInt Syntax + // Byte Aap + int i = 0; + S7CommPlusAlarmAssociatedValue pv; + foreach(var b in blobs) + { + var bytes = b.GetValue(); + switch (b.BlobRootId) + { + case (Ids.TI_BOOL): + pv = new S7CommPlusAlarmAssociatedValue(b.BlobRootId); + pv.SetBool(bytes[0] != 0); + av.SetSDValue(pv, i); + break; + case (Ids.TI_BYTE): + pv = new S7CommPlusAlarmAssociatedValue(b.BlobRootId); + pv.SetInt(bytes[0]); + av.SetSDValue(pv, i); + break; + case (Ids.TI_CHAR): + pv = new S7CommPlusAlarmAssociatedValue(b.BlobRootId); + pv.SetString(Encoding.GetEncoding("ISO-8859-1").GetString(bytes, 0, 1)); + av.SetSDValue(pv, i); + break; + case (Ids.TI_WORD): + pv = new S7CommPlusAlarmAssociatedValue(b.BlobRootId); + pv.SetInt(Utils.GetUInt16(bytes, 0)); + av.SetSDValue(pv, i); + break; + case (Ids.TI_INT): + pv = new S7CommPlusAlarmAssociatedValue(b.BlobRootId); + pv.SetInt(Utils.GetInt16(bytes, 0)); + av.SetSDValue(pv, i); + break; + case (Ids.TI_DWORD): + pv = new S7CommPlusAlarmAssociatedValue(b.BlobRootId); + pv.SetInt(Utils.GetUInt32(bytes, 0)); + av.SetSDValue(pv, i); + break; + case (Ids.TI_DINT): + pv = new S7CommPlusAlarmAssociatedValue(b.BlobRootId); + pv.SetInt(Utils.GetInt32(bytes, 0)); + av.SetSDValue(pv, i); + break; + case (Ids.TI_REAL): + pv = new S7CommPlusAlarmAssociatedValue(b.BlobRootId); + pv.SetReal(Utils.GetFloat(bytes, 0)); + av.SetSDValue(pv, i); + break; + case (Ids.TI_LREAL): + pv = new S7CommPlusAlarmAssociatedValue(b.BlobRootId); + pv.SetReal(Utils.GetDouble(bytes, 0)); + av.SetSDValue(pv, i); + break; + case (Ids.TI_USINT): + pv = new S7CommPlusAlarmAssociatedValue(b.BlobRootId); + pv.SetInt(bytes[0]); + av.SetSDValue(pv, i); + break; + case (Ids.TI_UINT): + pv = new S7CommPlusAlarmAssociatedValue(b.BlobRootId); + pv.SetInt(Utils.GetUInt16(bytes, 0)); + av.SetSDValue(pv, i); + break; + case (Ids.TI_UDINT): + pv = new S7CommPlusAlarmAssociatedValue(b.BlobRootId); + pv.SetInt(Utils.GetUInt32(bytes, 0)); + av.SetSDValue(pv, i); + break; + case (Ids.TI_SINT): + pv = new S7CommPlusAlarmAssociatedValue(b.BlobRootId); + pv.SetInt((sbyte)bytes[0]); + av.SetSDValue(pv, i); + break; + case (Ids.TI_WCHAR): + pv = new S7CommPlusAlarmAssociatedValue(b.BlobRootId); + pv.SetString(((char)Utils.GetUInt16(bytes, 0)).ToString()); + av.SetSDValue(pv, i); + break; + default: + if (b.BlobRootId > Ids.TI_STRING_START && b.BlobRootId <= Ids.TI_STRING_END) + { + //byte s_maxlen = bytes[0]; // Don't need this value + byte s_actlen = bytes[1]; + pv = new S7CommPlusAlarmAssociatedValue(Ids.TI_STRING); + pv.SetString(Encoding.GetEncoding("ISO-8859-1").GetString(bytes, 2, s_actlen)); + av.SetSDValue(pv, i); + } + else if (b.BlobRootId > Ids.TI_WSTRING_START && b.BlobRootId <= Ids.TI_WSTRING_END) + { + //int ws_maxlen = Utils.GetUInt16(bytes, 0); // Don't need this value + int ws_actlen = Utils.GetUInt16(bytes, 2); + pv = new S7CommPlusAlarmAssociatedValue(Ids.TI_WSTRING); + pv.SetString(Encoding.BigEndianUnicode.GetString(bytes, 4, ws_actlen * 2)); + av.SetSDValue(pv, i); + } + break; + } + i++; + // All other elements have no value + if (i > 10) + { + break; + } + } + return av; + } + + private void TrySetPackedStandardAssociatedValues(ValueBlob[] blobs) + { + if (blobs == null || blobs.Length < 2) + { + return; + } + + // TIA SupplyDataFormatterImpl.FormatPlusAlarmTexts uses this shape + // for standard element-type placeholders such as @2W%d@. + if (blobs[0].BlobRootId != 0 || blobs[1].BlobRootId != 0) + { + return; + } + + for (var i = 2; i < blobs.Length; i++) + { + if (blobs[i].BlobRootId != 0) + { + return; + } + } + + var bytes = blobs[1].GetValue(); + if (bytes == null || bytes.Length == 0) + { + return; + } + + PackedStandardAssociatedValues = new byte[bytes.Length]; + Array.Copy(bytes, PackedStandardAssociatedValues, bytes.Length); + } + + private void SetSDValue(S7CommPlusAlarmAssociatedValue v, int index) + { + switch(index) + { + case 1: SD_1 = v; break; + case 2: SD_2 = v; break; + case 3: SD_3 = v; break; + case 4: SD_4 = v; break; + case 5: SD_5 = v; break; + case 6: SD_6 = v; break; + case 7: SD_7 = v; break; + case 8: SD_8 = v; break; + case 9: SD_9 = v; break; + case 10: SD_10 = v; break; + } + } + + private static int GetElementTypeLength(char elementType) + { + switch (elementType) + { + case 'B': + case 'Y': + case 'C': + return 1; + case 'W': + case 'I': + return 2; + case 'X': + case 'D': + case 'R': + return 4; + case 'O': + return 8; + default: + return 0; + } + } + } + + public class S7CommPlusAlarmAssociatedValue + { + bool ValueBool; + Int64 ValueInt; + double ValueReal; + string ValueString; + + public uint TypeInfo; + // Allowed types in plc program: Bool, Byte, Char, DInt, DWord, Int, LReal, Real, SInt, String, UDInt, UInt, WChar, Word, WString + // Break down to .Net types which can handle all these values: Bool, Int64, double, string + + public S7CommPlusAlarmAssociatedValue(uint typeinfo) + { + TypeInfo = typeinfo; + } + + public void SetBool(bool value) + { + ValueBool = value; + } + + public void SetInt(Int64 value) + { + ValueInt = value; + } + + public void SetReal(double value) + { + ValueReal = value; + } + + public void SetString(string value) + { + ValueString = value; + } + + public bool IsString + { + get + { + return TypeInfo == Ids.TI_CHAR + || TypeInfo == Ids.TI_WCHAR + || TypeInfo == Ids.TI_STRING + || TypeInfo == Ids.TI_WSTRING; + } + } + + public bool IsReal + { + get + { + return TypeInfo == Ids.TI_REAL || TypeInfo == Ids.TI_LREAL; + } + } + + public string GetString() + { + return ValueString ?? String.Empty; + } + + public double GetReal() + { + return IsReal ? ValueReal : GetSignedInteger(); + } + + internal int GetIntegerByElementType(char elementType) + { + switch (elementType) + { + case 'B': + return (int)(ValueBool ? 1 : GetUnsignedInteger() & 0x1); + case 'Y': + case 'C': + return (int)(GetUnsignedInteger() & 0xFF); + case 'W': + case 'I': + return (int)(GetUnsignedInteger() & 0xFFFF); + case 'X': + case 'D': + case 'R': + return unchecked((int)(GetUnsignedInteger() & 0xFFFFFFFF)); + default: + return unchecked((int)GetUnsignedInteger()); + } + } + + internal double GetRealByElementType(char elementType) + { + if (elementType == 'O' || IsReal) + { + return ValueReal; + } + + return GetIntegerByElementType(elementType); + } + + public Int64 GetSignedInteger() + { + switch (TypeInfo) + { + case (Ids.TI_BOOL): + return ValueBool ? 1 : 0; + case (Ids.TI_BYTE): + case (Ids.TI_USINT): + return unchecked((sbyte)(byte)ValueInt); + case (Ids.TI_WORD): + case (Ids.TI_UINT): + return unchecked((short)(ushort)ValueInt); + case (Ids.TI_DWORD): + case (Ids.TI_UDINT): + return unchecked((int)(uint)ValueInt); + case (Ids.TI_INT): + case (Ids.TI_DINT): + case (Ids.TI_SINT): + return ValueInt; + case (Ids.TI_REAL): + case (Ids.TI_LREAL): + return Convert.ToInt64(ValueReal, CultureInfo.InvariantCulture); + default: + return ValueInt; + } + } + + public UInt64 GetUnsignedInteger() + { + switch (TypeInfo) + { + case (Ids.TI_BOOL): + return ValueBool ? 1u : 0u; + case (Ids.TI_BYTE): + case (Ids.TI_USINT): + case (Ids.TI_SINT): + return (byte)ValueInt; + case (Ids.TI_WORD): + case (Ids.TI_UINT): + case (Ids.TI_INT): + return (ushort)ValueInt; + case (Ids.TI_DWORD): + case (Ids.TI_UDINT): + case (Ids.TI_DINT): + return (uint)ValueInt; + case (Ids.TI_REAL): + case (Ids.TI_LREAL): + return Convert.ToUInt64(ValueReal, CultureInfo.InvariantCulture); + default: + return (UInt64)Math.Max(0, ValueInt); + } + } + + public override string ToString() + { + string s = String.Empty; + switch (TypeInfo) + { + case (Ids.TI_BOOL): + s = ValueBool.ToString(); + break; + case (Ids.TI_BYTE): + s = ValueInt.ToString(); + break; + case (Ids.TI_CHAR): + s = ValueString; + break; + case (Ids.TI_WORD): + s = ValueInt.ToString(); + break; + case (Ids.TI_INT): + s = ValueInt.ToString(); + break; + case (Ids.TI_DWORD): + s = ValueInt.ToString(); + break; + case (Ids.TI_DINT): + s = ValueInt.ToString(); + break; + case (Ids.TI_REAL): + s = ValueReal.ToString(); + break; + case (Ids.TI_LREAL): + s = ValueReal.ToString(); + break; + case (Ids.TI_USINT): + s = ValueInt.ToString(); + break; + case (Ids.TI_UINT): + s = ValueInt.ToString(); + break; + case (Ids.TI_UDINT): + s = ValueInt.ToString(); + break; + case (Ids.TI_SINT): + s = ValueInt.ToString(); + break; + case (Ids.TI_WCHAR): + s = ValueString; + break; + case (Ids.TI_STRING): + s = ValueString; + break; + case (Ids.TI_WSTRING): + s = ValueString; + break; + } + return s; + } + } +} diff --git a/src/S7CommPlusDriver/Alarming/AlarmsHmiInfo.cs b/src/S7CommPlusDriver/Alarming/S7CommPlusAlarmHmiInfo.cs similarity index 93% rename from src/S7CommPlusDriver/Alarming/AlarmsHmiInfo.cs rename to src/S7CommPlusDriver/Alarming/S7CommPlusAlarmHmiInfo.cs index b07f157..7af0eba 100644 --- a/src/S7CommPlusDriver/Alarming/AlarmsHmiInfo.cs +++ b/src/S7CommPlusDriver/Alarming/S7CommPlusAlarmHmiInfo.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -19,7 +19,7 @@ namespace S7CommPlusDriver.Alarming { // 7813 = DAI.HmiInfo - public class AlarmsHmiInfo + public class S7CommPlusAlarmHmiInfo { public ushort SyntaxId; public ushort Version; @@ -35,7 +35,7 @@ public class AlarmsHmiInfo public override string ToString() { - string s = "" + Environment.NewLine; + string s = "" + Environment.NewLine; s += "" + SyntaxId.ToString() + "" + Environment.NewLine; s += "" + Version.ToString() + "" + Environment.NewLine; s += "" + ClientAlarmId.ToString() + "" + Environment.NewLine; @@ -53,7 +53,7 @@ public override string ToString() s += "" + Flags.ToString() + "" + Environment.NewLine; } } - s += "" + Environment.NewLine; + s += "" + Environment.NewLine; return s; } @@ -80,9 +80,9 @@ public int Deserialize(Stream buffer) return ret; } - public static AlarmsHmiInfo FromValueBlob(ValueBlob blob) + internal static S7CommPlusAlarmHmiInfo FromValueBlob(ValueBlob blob) { - var hmiinfo = new AlarmsHmiInfo(); + var hmiinfo = new S7CommPlusAlarmHmiInfo(); var barr = blob.GetValue(); uint pos = 0; hmiinfo.SyntaxId = Utils.GetUInt16(barr, pos); diff --git a/src/S7CommPlusDriver/Alarming/AlarmsAsCgs.cs b/src/S7CommPlusDriver/Alarming/S7CommPlusAlarmStateChange.cs similarity index 79% rename from src/S7CommPlusDriver/Alarming/AlarmsAsCgs.cs rename to src/S7CommPlusDriver/Alarming/S7CommPlusAlarmStateChange.cs index 960ca8b..fe02c06 100644 --- a/src/S7CommPlusDriver/Alarming/AlarmsAsCgs.cs +++ b/src/S7CommPlusDriver/Alarming/S7CommPlusAlarmStateChange.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -17,7 +17,7 @@ namespace S7CommPlusDriver.Alarming { - public class AlarmsAsCgs + public class S7CommPlusAlarmStateChange { public enum SubtypeIds { @@ -28,28 +28,28 @@ public enum SubtypeIds public byte AllStatesInfo; public DateTime Timestamp; - public AlarmsAssociatedValues AssociatedValues; + public S7CommPlusAlarmAssociatedValues AssociatedValues; public DateTime AckTimestamp; public override string ToString() { - string s = "" + Environment.NewLine; + string s = "" + Environment.NewLine; s += "" + SubtypeId.ToString() + "" + Environment.NewLine; s += "" + ((SubtypeIds)SubtypeId).ToString() + "" + Environment.NewLine; s += "" + AllStatesInfo.ToString() + "" + Environment.NewLine; s += "" + Environment.NewLine + AssociatedValues.ToString() + "" + Environment.NewLine; s += "" + Timestamp.ToString() + "" + Environment.NewLine; s += "" + AckTimestamp.ToString() + "" + Environment.NewLine; - s += "" + Environment.NewLine; + s += "" + Environment.NewLine; return s; } - public static AlarmsAsCgs FromValueStruct(ValueStruct str) + internal static S7CommPlusAlarmStateChange FromValueStruct(ValueStruct str) { - var asCgs = new AlarmsAsCgs(); + var asCgs = new S7CommPlusAlarmStateChange(); asCgs.AllStatesInfo = ((ValueUSInt)str.GetStructElement(Ids.AS_CGS_AllStatesInfo)).GetValue(); asCgs.Timestamp = Utils.DtFromValueTimestamp(((ValueTimestamp)str.GetStructElement(Ids.AS_CGS_Timestamp)).GetValue()); - asCgs.AssociatedValues = AlarmsAssociatedValues.FromValueBlob(((ValueBlobArray)str.GetStructElement(Ids.AS_CGS_AssociatedValues))); + asCgs.AssociatedValues = S7CommPlusAlarmAssociatedValues.FromValueBlob(((ValueBlobArray)str.GetStructElement(Ids.AS_CGS_AssociatedValues))); asCgs.AckTimestamp = Utils.DtFromValueTimestamp(((ValueTimestamp)str.GetStructElement(Ids.AS_CGS_AckTimestamp)).GetValue()); return asCgs; } diff --git a/src/S7CommPlusDriver/Alarming/S7CommPlusAlarmTextFormatter.cs b/src/S7CommPlusDriver/Alarming/S7CommPlusAlarmTextFormatter.cs new file mode 100644 index 0000000..09ad307 --- /dev/null +++ b/src/S7CommPlusDriver/Alarming/S7CommPlusAlarmTextFormatter.cs @@ -0,0 +1,645 @@ +#region License +/****************************************************************************** + * S7CommPlusDriver + * + * Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de + * + * This file is part of S7CommPlusDriver. + * + * S7CommPlusDriver is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + /****************************************************************************/ +#endregion + +using System; +using System.Globalization; +using System.Text; + +namespace S7CommPlusDriver.Alarming +{ + internal static class S7CommPlusAlarmTextFormatter + { + private const int MaxTextListRecursionDepth = 8; + + public static string Format(string text, S7CommPlusAlarmAssociatedValues associatedValues, int languageId) + { + return Format(text, associatedValues, languageId, null); + } + + public static string Format( + string text, + S7CommPlusAlarmAssociatedValues associatedValues, + int languageId, + Func textListResolver) + { + return Format(text, associatedValues, languageId, textListResolver, 0); + } + + private static string Format( + string text, + S7CommPlusAlarmAssociatedValues associatedValues, + int languageId, + Func textListResolver, + int recursionDepth) + { + if (String.IsNullOrEmpty(text) || associatedValues == null) + { + return text ?? String.Empty; + } + + var culture = GetCulture(languageId); + var builder = new StringBuilder(text.Length); + var copiedUntil = 0; + var searchFrom = 0; + while (searchFrom < text.Length) + { + var start = text.IndexOf('@', searchFrom); + if (start < 0) + { + break; + } + + if (!TryParsePlaceholder(text, start, out var placeholder)) + { + searchFrom = start + 1; + continue; + } + + var originalBlock = text.Substring(start, placeholder.End - start); + var value = associatedValues.GetValue(placeholder.Index); + var packedStandardValue = 0; + var hasPackedStandardValue = placeholder.ElementType.HasValue + && associatedValues.TryGetPackedStandardInteger(placeholder.Index, placeholder.ElementType.Value, out packedStandardValue); + + if (placeholder.IsTextList) + { + string textListValue; + if (hasPackedStandardValue) + { + textListValue = textListResolver?.Invoke(placeholder.TextListName, packedStandardValue, languageId); + } + else + { + if (value == null) + { + searchFrom = placeholder.End; + continue; + } + + textListValue = textListResolver?.Invoke( + placeholder.TextListName, + value.GetIntegerByElementType(placeholder.ElementType.GetValueOrDefault(GetElementType(value))), + languageId); + } + + if (textListValue == null) + { + searchFrom = placeholder.End; + continue; + } + if (recursionDepth < MaxTextListRecursionDepth) + { + textListValue = Format(textListValue, associatedValues, languageId, textListResolver, recursionDepth + 1); + } + + builder.Append(text, copiedUntil, start - copiedUntil); + builder.Append(textListValue); + copiedUntil = placeholder.End; + searchFrom = placeholder.End; + continue; + } + + if (value == null && !hasPackedStandardValue) + { + searchFrom = placeholder.End; + continue; + } + + var elementType = placeholder.ElementType.HasValue + ? placeholder.ElementType.Value + : GetElementType(value); + var padChar = placeholder.WidthText.StartsWith("0", StringComparison.Ordinal) ? '0' : ' '; + var width = ParseOptionalInt(placeholder.WidthText).GetValueOrDefault(); + var precision = ParseOptionalInt(placeholder.PrecisionText); + var replacement = hasPackedStandardValue + ? FormatPackedStandardValue(associatedValues, placeholder.Index, packedStandardValue, elementType, placeholder.Format, width, precision, padChar, culture, originalBlock) + : FormatValue(value, elementType, placeholder.Format, width, precision, padChar, culture, originalBlock); + + builder.Append(text, copiedUntil, start - copiedUntil); + builder.Append(replacement); + copiedUntil = placeholder.End; + searchFrom = placeholder.End; + } + + if (copiedUntil == 0) + { + return text; + } + + builder.Append(text, copiedUntil, text.Length - copiedUntil); + return builder.ToString(); + } + + private static bool TryParsePlaceholder(string text, int start, out AlarmTextPlaceholder placeholder) + { + placeholder = default; + var pos = start + 1; + if (pos >= text.Length || !Char.IsDigit(text[pos])) + { + return false; + } + + var indexStart = pos; + while (pos < text.Length && Char.IsDigit(text[pos])) + { + pos++; + } + + if (!Int32.TryParse(text.Substring(indexStart, pos - indexStart), NumberStyles.None, CultureInfo.InvariantCulture, out var index)) + { + return false; + } + + char? elementType = null; + if (pos < text.Length && IsElementType(text[pos])) + { + elementType = text[pos]; + pos++; + } + + if (pos >= text.Length || text[pos] != '%') + { + return false; + } + pos++; + + var widthStart = pos; + while (pos < text.Length && Char.IsDigit(text[pos])) + { + pos++; + } + var widthText = text.Substring(widthStart, pos - widthStart); + + var precisionText = String.Empty; + if (pos < text.Length && text[pos] == '.') + { + pos++; + var precisionStart = pos; + while (pos < text.Length && Char.IsDigit(text[pos])) + { + pos++; + } + precisionText = text.Substring(precisionStart, pos - precisionStart); + } + + if (pos >= text.Length) + { + return false; + } + + if (text[pos] == 't' && pos + 1 < text.Length && text[pos + 1] == '#') + { + var end = text.IndexOf('@', pos + 2); + if (end < 0) + { + return false; + } + + var textListName = text.Substring(pos + 2, end - pos - 2); + placeholder = new AlarmTextPlaceholder(index, elementType, widthText, precisionText, '\0', end + 1, true, textListName); + return true; + } + + if (!IsDisplayFormat(text[pos])) + { + return false; + } + + var format = text[pos]; + pos++; + if (pos >= text.Length || text[pos] != '@') + { + return false; + } + + placeholder = new AlarmTextPlaceholder(index, elementType, widthText, precisionText, format, pos + 1, false, String.Empty); + return true; + } + + private static bool IsElementType(char value) + { + switch (value) + { + case 'B': + case 'C': + case 'D': + case 'I': + case 'O': + case 'R': + case 'T': + case 'W': + case 'X': + case 'Y': + return true; + default: + return false; + } + } + + private static bool IsDisplayFormat(char value) + { + switch (value) + { + case 'b': + case 'd': + case 'f': + case 's': + case 'u': + case 'X': + case 'x': + return true; + default: + return false; + } + } + + private readonly struct AlarmTextPlaceholder + { + public AlarmTextPlaceholder( + int index, + char? elementType, + string widthText, + string precisionText, + char format, + int end, + bool isTextList, + string textListName) + { + Index = index; + ElementType = elementType; + WidthText = widthText; + PrecisionText = precisionText; + Format = format; + End = end; + IsTextList = isTextList; + TextListName = textListName; + } + + public int Index { get; } + public char? ElementType { get; } + public string WidthText { get; } + public string PrecisionText { get; } + public char Format { get; } + public int End { get; } + public bool IsTextList { get; } + public string TextListName { get; } + } + + private static string FormatValue( + S7CommPlusAlarmAssociatedValue value, + char elementType, + char format, + int width, + int? precision, + char padChar, + CultureInfo culture, + string originalBlock) + { + switch (format) + { + case 'd': + return FormatDecimalSigned(MakeSigned(value.GetIntegerByElementType(elementType), elementType), width, padChar); + case 'u': + return FormatDecimalUnsigned((uint)value.GetIntegerByElementType(elementType), width, padChar); + case 'x': + case 'X': + return FormatHex(value.GetIntegerByElementType(elementType), width); + case 'b': + return FormatBinary(value.GetIntegerByElementType(elementType), width); + case 'f': + if (elementType != 'R' && elementType != 'X' && elementType != 'D' && elementType != 'O') + { + return originalBlock; + } + return FormatFloatingPoint(value.GetRealByElementType(elementType), precision, width, padChar, culture); + case 's': + if (value.IsString) + { + return ApplyMinimumLength(value.GetString(), width, padChar); + } + return FormatAsString(value.GetIntegerByElementType(elementType), width, padChar); + default: + return originalBlock; + } + } + + private static string FormatPackedStandardValue( + S7CommPlusAlarmAssociatedValues associatedValues, + int position, + int value, + char elementType, + char format, + int width, + int? precision, + char padChar, + CultureInfo culture, + string originalBlock) + { + switch (format) + { + case 'd': + return FormatDecimalSigned(MakeSigned(value, elementType), width, padChar); + case 'u': + return FormatDecimalUnsigned((uint)value, width, padChar); + case 'x': + case 'X': + return FormatHex(value, width); + case 'b': + return FormatBinary(value, width); + case 'f': + if (elementType != 'R' && elementType != 'X' && elementType != 'D' && elementType != 'O') + { + return originalBlock; + } + + if (!associatedValues.TryGetPackedStandardReal(position, elementType, out var realValue)) + { + return originalBlock; + } + + return FormatFloatingPoint(realValue, precision, width, padChar, culture); + case 's': + return FormatAsString(value, width, padChar); + default: + return originalBlock; + } + } + + private static string FormatBinary(int value, int width) + { + var text = MakeBitString(value).PadLeft(width, '0'); + return "2#" + text; + } + + private static string FormatDecimalSigned(int value, int width, char padChar) + { + string text; + if (value >= 0 || padChar == ' ') + { + text = value.ToString("D", CultureInfo.CurrentCulture); + return text.PadLeft(width, padChar); + } + + value *= -1; + text = value.ToString("D", CultureInfo.CurrentCulture); + if (width > 0) + { + width--; + } + + return "-" + text.PadLeft(width, padChar); + } + + private static string FormatDecimalUnsigned(uint value, int width, char padChar) + { + return value.ToString("D", CultureInfo.CurrentCulture).PadLeft(width, padChar); + } + + private static string FormatFloatingPoint(double value, int? precision, int width, char padChar, CultureInfo culture) + { + var decimals = precision.GetValueOrDefault(6); + var text = width <= 0 ? GetENumber(value, decimals, 12, culture) : GetENumber(value, decimals, width, culture); + if (width <= 0) + { + return text; + } + + if (value >= 0.0 || padChar == ' ') + { + return text.PadLeft(width, padChar); + } + + text = text.Substring(1); + width--; + return "-" + text.PadLeft(width, padChar); + } + + private static string FormatHex(int value, int width) + { + var text = value.ToString("X", CultureInfo.CurrentCulture).PadLeft(width, '0'); + return "16#" + text; + } + + private static string FormatAsString(int value, int width, char padChar) + { + return GetStringOfValue(value).PadLeft(width, padChar); + } + + private static string ApplyMinimumLength(string value, int length, char padChar) + { + if (length <= value.Length) + { + return value; + } + + return value.PadLeft(length, padChar); + } + + private static string GetENumber(double number, int decimals, int positiveNumbers, CultureInfo culture) + { + if (number > 1.0) + { + if (number > Math.Pow(10.0, positiveNumbers)) + { + return number.ToString("e" + decimals.ToString(CultureInfo.InvariantCulture), culture); + } + return Math.Round(number, decimals, MidpointRounding.AwayFromZero).ToString(culture); + } + + if (number > -1.0) + { + if (Math.Round(number, decimals, MidpointRounding.AwayFromZero) > 0.0) + { + return Math.Round(number, decimals, MidpointRounding.AwayFromZero).ToString(culture); + } + if (number == 0.0) + { + return number.ToString("f1", culture); + } + return number.ToString("e" + decimals.ToString(CultureInfo.InvariantCulture), culture); + } + + if (number < Math.Pow(10.0, positiveNumbers) * -1.0) + { + return number.ToString("e" + decimals.ToString(CultureInfo.InvariantCulture), culture); + } + + return Math.Round(number, decimals, MidpointRounding.AwayFromZero).ToString(culture); + } + + private static char GetElementType(S7CommPlusAlarmAssociatedValue value) + { + switch (value.TypeInfo) + { + case Ids.TI_BOOL: + return 'B'; + case Ids.TI_BYTE: + case Ids.TI_CHAR: + case Ids.TI_USINT: + case Ids.TI_SINT: + return 'Y'; + case Ids.TI_WORD: + case Ids.TI_UINT: + case Ids.TI_WCHAR: + return 'W'; + case Ids.TI_INT: + return 'I'; + case Ids.TI_DWORD: + case Ids.TI_UDINT: + return 'X'; + case Ids.TI_DINT: + return 'D'; + case Ids.TI_REAL: + return 'R'; + case Ids.TI_LREAL: + return 'O'; + case Ids.TI_STRING: + case Ids.TI_WSTRING: + return 'T'; + default: + if (value.TypeInfo > Ids.TI_STRING_START && value.TypeInfo <= Ids.TI_WSTRING_END) + { + return 'T'; + } + return 'X'; + } + } + + private static string GetStringOfValue(int value) + { + char[] chars = new char[4] { ' ', ' ', ' ', ' ' }; + char[] raw = new char[4]; + raw[3] = (char)(value & 0xFF); + raw[2] = (char)((value & 0xFF00) >> 8); + raw[1] = (char)((value & 0xFF0000) >> 16); + raw[0] = (char)((value & 0xFF000000u) >> 24); + for (int i = 3; i > -1; i--) + { + if (raw[i] >= ' ' && (raw[i] <= '~' || raw[i] >= '\u00a0')) + { + chars[i] = raw[i]; + } + } + + return new string(chars).Trim(); + } + + private static int MakeSigned(int value, char elementType) + { + switch (GetElementTypeLength(elementType)) + { + case 1: + if ((value & 0x80) != 0) + { + return value | -256; + } + break; + case 2: + if ((value & 0x8000) != 0) + { + return value | -65536; + } + break; + case 3: + if ((value & 0x800000) != 0) + { + return value | -16777216; + } + break; + } + + return value; + } + + private static int GetElementTypeLength(char elementType) + { + switch (elementType) + { + case 'B': + case 'Y': + case 'C': + return 1; + case 'W': + case 'I': + return 2; + case 'X': + case 'D': + case 'R': + return 4; + case 'O': + return 8; + default: + return 0; + } + } + + private static string MakeBitString(int value) + { + var builder = new StringBuilder(); + var hasOne = false; + uint mask = 0x80000000; + for (var i = 0; i < 32; i++) + { + if ((value & mask) == 0) + { + if (hasOne) + { + builder.Append('0'); + } + } + else + { + builder.Append('1'); + hasOne = true; + } + mask >>= 1; + } + + if (builder.Length == 0) + { + builder.Append('0'); + } + + return builder.ToString(); + } + + private static int? ParseOptionalInt(string value) + { + if (String.IsNullOrEmpty(value)) + { + return null; + } + + if (Int32.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out var result)) + { + return result; + } + + return null; + } + + private static CultureInfo GetCulture(int languageId) + { + try + { + return CultureInfo.GetCultureInfo(languageId); + } + catch (CultureNotFoundException) + { + return CultureInfo.InvariantCulture; + } + } + } +} diff --git a/src/S7CommPlusDriver/Alarming/S7CommPlusAlarmTexts.cs b/src/S7CommPlusDriver/Alarming/S7CommPlusAlarmTexts.cs new file mode 100644 index 0000000..8bc97a2 --- /dev/null +++ b/src/S7CommPlusDriver/Alarming/S7CommPlusAlarmTexts.cs @@ -0,0 +1,173 @@ +#region License +/****************************************************************************** + * S7CommPlusDriver + * + * Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de + * + * This file is part of S7CommPlusDriver. + * + * S7CommPlusDriver is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + /****************************************************************************/ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace S7CommPlusDriver.Alarming +{ + public class S7CommPlusAlarmTexts + { + public int LanguageId; + public string Infotext = String.Empty; + public string AlarmText = String.Empty; + public string AdditionalText1 = String.Empty; + public string AdditionalText2 = String.Empty; + public string AdditionalText3 = String.Empty; + public string AdditionalText4 = String.Empty; + public string AdditionalText5 = String.Empty; + public string AdditionalText6 = String.Empty; + public string AdditionalText7 = String.Empty; + public string AdditionalText8 = String.Empty; + public string AdditionalText9 = String.Empty; + + // These two values we get in addition when browsing for the alarmtexts + // Don't know if they are useful for something. + public ushort UnknownValue1; + public ushort UnknownValue2; + + internal static S7CommPlusAlarmTexts FromNotificationBlob(ValueBlobSparseArray blob, int languageId) + { + var languages = FromNotificationBlobAllLanguages(blob); + if (languages.TryGetValue(languageId, out var texts)) + { + return texts; + } + + return new S7CommPlusAlarmTexts { LanguageId = languageId }; + } + + internal static Dictionary FromNotificationBlobAllLanguages(ValueBlobSparseArray blob) + { + string s; + int lcid; + int textid; + var textsByLanguage = new Dictionary(); + foreach (var v in blob.Value) + { + s = Utils.GetUtfString(v.Value.value, 0, (uint)v.Value.value.Length); + // Values in older CPUs, from: 0xa09c8001..0xa09c800b (2694610945..2694610955) + // Current CPUs use: 0x04070001..0x0407000b ( 67567617.. 67567627) + // Where the left word is the language ID, 0x0407 = 1031, and the right word is the text id. + // The blob may contain several languages. If you need them all, you need to call this multiple times. + lcid = (int)(v.Key >> 16); + textid = (int)(v.Key & 0xffff); + if (!textsByLanguage.TryGetValue(lcid, out var at)) + { + at = new S7CommPlusAlarmTexts { LanguageId = lcid }; + textsByLanguage.Add(lcid, at); + } + + switch (textid) + { + case 1: + at.Infotext = s; + break; + case 2: + at.AlarmText = s; + break; + case 3: + at.AdditionalText1 = s; + break; + case 4: + at.AdditionalText2 = s; + break; + case 5: + at.AdditionalText3 = s; + break; + case 6: + at.AdditionalText4 = s; + break; + case 7: + at.AdditionalText5 = s; + break; + case 8: + at.AdditionalText6 = s; + break; + case 9: + at.AdditionalText7 = s; + break; + case 10: + at.AdditionalText8 = s; + break; + case 11: + at.AdditionalText9 = s; + break; + } + } + + return textsByLanguage; + } + + internal void ApplyAssociatedValues(S7CommPlusAlarmAssociatedValues associatedValues) + { + ApplyAssociatedValues(associatedValues, null); + } + + internal void ApplyAssociatedValues(S7CommPlusAlarmAssociatedValues associatedValues, Func textListResolver) + { + Infotext = FormatText(Infotext, associatedValues, textListResolver); + AlarmText = FormatText(AlarmText, associatedValues, textListResolver); + AdditionalText1 = FormatText(AdditionalText1, associatedValues, textListResolver); + AdditionalText2 = FormatText(AdditionalText2, associatedValues, textListResolver); + AdditionalText3 = FormatText(AdditionalText3, associatedValues, textListResolver); + AdditionalText4 = FormatText(AdditionalText4, associatedValues, textListResolver); + AdditionalText5 = FormatText(AdditionalText5, associatedValues, textListResolver); + AdditionalText6 = FormatText(AdditionalText6, associatedValues, textListResolver); + AdditionalText7 = FormatText(AdditionalText7, associatedValues, textListResolver); + AdditionalText8 = FormatText(AdditionalText8, associatedValues, textListResolver); + AdditionalText9 = FormatText(AdditionalText9, associatedValues, textListResolver); + } + + private string FormatText(string text, S7CommPlusAlarmAssociatedValues associatedValues) + { + return FormatText(text, associatedValues, null); + } + + private string FormatText(string text, S7CommPlusAlarmAssociatedValues associatedValues, Func textListResolver) + { + return S7CommPlusAlarmTextFormatter.Format(text, associatedValues, LanguageId, textListResolver).Replace("\\n", Environment.NewLine); + } + + public override string ToString() + { + string s = "" + Environment.NewLine; + s += "" + Infotext.ToString() + "" + Environment.NewLine; + s += "" + AlarmText.ToString() + "" + Environment.NewLine; + s += "" + AdditionalText1.ToString() + "" + Environment.NewLine; + s += "" + AdditionalText2.ToString() + "" + Environment.NewLine; + s += "" + AdditionalText3.ToString() + "" + Environment.NewLine; + s += "" + AdditionalText4.ToString() + "" + Environment.NewLine; + s += "" + AdditionalText5.ToString() + "" + Environment.NewLine; + s += "" + AdditionalText6.ToString() + "" + Environment.NewLine; + s += "" + AdditionalText7.ToString() + "" + Environment.NewLine; + s += "" + AdditionalText8.ToString() + "" + Environment.NewLine; + s += "" + AdditionalText9.ToString() + "" + Environment.NewLine; + s += "" + Environment.NewLine; + return s; + } + + internal static S7CommPlusAlarmTexts FirstOrEmpty(Dictionary textsByLanguage) + { + if (textsByLanguage == null || textsByLanguage.Count == 0) + { + return new S7CommPlusAlarmTexts(); + } + + return textsByLanguage.OrderBy(pair => pair.Key).First().Value; + } + } +} diff --git a/src/S7CommPlusDriver/Alarming/AlarmsMultipleStai.cs b/src/S7CommPlusDriver/Alarming/S7CommPlusConfiguredAlarmInfo.cs similarity index 87% rename from src/S7CommPlusDriver/Alarming/AlarmsMultipleStai.cs rename to src/S7CommPlusDriver/Alarming/S7CommPlusConfiguredAlarmInfo.cs index 084e99b..d305385 100644 --- a/src/S7CommPlusDriver/Alarming/AlarmsMultipleStai.cs +++ b/src/S7CommPlusDriver/Alarming/S7CommPlusConfiguredAlarmInfo.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -18,7 +18,7 @@ namespace S7CommPlusDriver.Alarming { - public class AlarmsMultipleStai + internal class S7CommPlusConfiguredAlarmInfo { public ushort Alid; public ushort AlarmDomain; // 1=Systemdiagnose, 2=Security, 256..272 = UserClass_0..UserClass_16 @@ -26,14 +26,14 @@ public class AlarmsMultipleStai public byte AlarmEnabled; //0=No, 1=Yes public ushort HmiInfoLength; - public AlarmsHmiInfo HmiInfo; + public S7CommPlusAlarmHmiInfo HmiInfo; public ushort LidCount; public uint[] Lids; public override string ToString() { string s = ""; - s += "" + Environment.NewLine; + s += "" + Environment.NewLine; s += "" + Alid.ToString() + "" + Environment.NewLine; s += "" + AlarmDomain.ToString() + "" + Environment.NewLine; s += "" + MessageType.ToString() + "" + Environment.NewLine; @@ -45,11 +45,11 @@ public override string ToString() { s += "" + li.ToString() + "" + Environment.NewLine; } - s += "" + Environment.NewLine; + s += "" + Environment.NewLine; return s; } - public int Deserialize(Stream buffer) + internal int Deserialize(Stream buffer) { int ret = 0; ret += S7p.DecodeUInt16(buffer, out Alid); @@ -57,7 +57,7 @@ public int Deserialize(Stream buffer) ret += S7p.DecodeUInt16(buffer, out MessageType); ret += S7p.DecodeByte(buffer, out AlarmEnabled); ret += S7p.DecodeUInt16(buffer, out HmiInfoLength); - HmiInfo = new AlarmsHmiInfo(); + HmiInfo = new S7CommPlusAlarmHmiInfo(); ret += HmiInfo.Deserialize(buffer); ret += S7p.DecodeUInt16(buffer, out LidCount); Lids = new uint[LidCount]; diff --git a/src/S7CommPlusDriver/App.config b/src/S7CommPlusDriver/App.config deleted file mode 100644 index 56efbc7..0000000 --- a/src/S7CommPlusDriver/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/S7CommPlusDriver/ClientApi/Browser.cs b/src/S7CommPlusDriver/ClientApi/Browser.cs index 1b9ccad..3b7ef2a 100644 --- a/src/S7CommPlusDriver/ClientApi/Browser.cs +++ b/src/S7CommPlusDriver/ClientApi/Browser.cs @@ -15,10 +15,11 @@ using System; using System.Collections.Generic; +using S7CommPlusDriver.Internal; namespace S7CommPlusDriver { - public class Browser + internal class Browser { VarRoot m_Root; List m_objs; @@ -62,13 +63,14 @@ public void BuildFlatList() { uint OptOffset = 0; uint NonOptOffset = 0; - AddFlatSubnodes(node, String.Empty, String.Empty, OptOffset, NonOptOffset); + AddFlatSubnodes(node, String.Empty, String.Empty, OptOffset, NonOptOffset, new List()); } } } - private void AddFlatSubnodes(Node node, string names, string accessIds, uint OptOffset, uint NonOptOffset) + private void AddFlatSubnodes(Node node, string names, string accessIds, uint OptOffset, uint NonOptOffset, List crcPath) { + var nextCrcPath = crcPath; switch (node.NodeType) { case eNodeType.Root: @@ -78,15 +80,21 @@ private void AddFlatSubnodes(Node node, string names, string accessIds, uint Opt case eNodeType.Array: names += node.Name; accessIds += "." + String.Format("{0:X}", node.AccessId); + nextCrcPath = ReplaceLastCrcSegmentWithArray(node, crcPath); break; case eNodeType.StructArray: names += node.Name; - // TODO: Special: Between an array-index and the access-id is an additional 1. It's not known if it's a fixed or variable value. + // Siemens symbolic paths include a literal ".1" between the struct-array index and the member access id. accessIds += "." + String.Format("{0:X}", node.AccessId) + ".1"; + nextCrcPath = ReplaceLastCrcSegmentWithArray(node, crcPath); break; default: names += "." + node.Name; accessIds += "." + String.Format("{0:X}", node.AccessId); + nextCrcPath = new List(crcPath) + { + S7CommPlusSymbolCrc.PathSegment.Member(node.Name, node.Softdatatype) + }; break; } @@ -100,6 +108,8 @@ private void AddFlatSubnodes(Node node, string names, string accessIds, uint Opt Name = names, AccessSequence = accessIds, Softdatatype = node.Softdatatype, + SymbolCrc = S7CommPlusSymbolCrc.ComputeFromSegments(nextCrcPath), + SymbolCrcPath = nextCrcPath, }; // If an Array element of basic datatype, the Vte is here from the parent array base element and offsets not valid here. if (node.NodeType == eNodeType.Array) @@ -172,16 +182,45 @@ private void AddFlatSubnodes(Node node, string names, string accessIds, uint Opt { if (sub.NodeType == eNodeType.Array) { - AddFlatSubnodes(sub, names, accessIds, OptOffset + sub.ArrayAdrOffsetOpt, NonOptOffset + sub.ArrayAdrOffsetNonOpt); + AddFlatSubnodes(sub, names, accessIds, OptOffset + sub.ArrayAdrOffsetOpt, NonOptOffset + sub.ArrayAdrOffsetNonOpt, nextCrcPath); } else { - AddFlatSubnodes(sub, names, accessIds, OptOffset, NonOptOffset); + AddFlatSubnodes(sub, names, accessIds, OptOffset, NonOptOffset, nextCrcPath); } } } } + private static List ReplaceLastCrcSegmentWithArray(Node node, List crcPath) + { + var nextCrcPath = new List(crcPath); + if (nextCrcPath.Count == 0) + { + return nextCrcPath; + } + + var previous = nextCrcPath[nextCrcPath.Count - 1]; + nextCrcPath[nextCrcPath.Count - 1] = S7CommPlusSymbolCrc.PathSegment.Array( + previous.MemberName, + node.Softdatatype, + GetArrayLowerBound(node.Vte?.OffsetInfoType)); + return nextCrcPath; + } + + internal static int GetArrayLowerBound(POffsetInfoType offsetInfoType) + { + if (offsetInfoType is IOffsetInfoType_1Dim oneDim) + { + return oneDim.GetArrayLowerBounds(); + } + if (offsetInfoType is IOffsetInfoType_MDim multiDim) + { + return multiDim.GetArrayLowerBounds(); + } + return 0; + } + public void BuildTree() { for (int i = 0; i < m_Root.Nodes.Count; i++) @@ -222,7 +261,7 @@ private void AddSubNodes(ref Node node, PObject o) }; node.Childs.Add(subnode); - // Process arrays. TODO: Put the processing to separate methods, to shorten this method. + // Process arrays before simple relation-based structs so array element offsets are materialized. if (vte.OffsetInfoType.Is1Dim()) { #region Struct/UDT or flat arrays with one dimension @@ -301,6 +340,11 @@ private void AddSubNodes(ref Node node, PObject o) actdimensions++; } } + if (ArrayElementCount == 0 || actdimensions == 0) + { + element_index++; + continue; + } string aname = ""; uint n = 1; @@ -416,7 +460,7 @@ private uint GetSizeOfDatatype(PVartypeListElement vte) switch (vte.Softdatatype) { case Softdatatype.S7COMMP_SOFTDATATYPE_BOOL: - // TODO: Bit Bool? + // BBOOL arrays are addressed byte-wise by the browser tree. return 1; case Softdatatype.S7COMMP_SOFTDATATYPE_BYTE: return 1; @@ -444,7 +488,6 @@ private uint GetSizeOfDatatype(PVartypeListElement vte) return 8; case Softdatatype.S7COMMP_SOFTDATATYPE_STRING: case Softdatatype.S7COMMP_SOFTDATATYPE_WSTRING: - // TODO: // If an array of String or WString, offsetinfo1 is the string length. // First though was, that offsetinfo2 is length including header of 2 bytes. // but with an Multidim Array [0..2, 0..1] of String[5] offsetinfo is 8, which is not @@ -507,10 +550,10 @@ private uint GetSizeOfDatatype(PVartypeListElement vte) case Softdatatype.S7COMMP_SOFTDATATYPE_EVENTATT: return 4; case Softdatatype.S7COMMP_SOFTDATATYPE_AOMAID: - // TODO: Not possible to define this type + // AOM reference types do not have a fixed scalar payload size. return 0; case Softdatatype.S7COMMP_SOFTDATATYPE_AOMLINK: - // TODO: Not possible to define this type + // AOM reference types do not have a fixed scalar payload size. return 0; case Softdatatype.S7COMMP_SOFTDATATYPE_EVENTHWINT: return 4; @@ -683,14 +726,16 @@ public class VarInfo { public string Name; public string AccessSequence; + public UInt32 SymbolCrc; public UInt32 Softdatatype; public UInt32 OptAddress; // Optimized access: Byte-Offset where the value is located when reading a complete DB content. public int OptBitoffset; // Optimized access: Bit-Offset where the value is located when reading a complete DB content. public UInt32 NonOptAddress; // NonOptimized access: Byte-Offset where the value is located when reading a complete DB content. public int NonOptBitoffset; // NonOptimized access: Bit-Offset where the value is located when reading a complete DB content. + internal List SymbolCrcPath; } - public enum eNodeType + internal enum eNodeType { Undefined = 0, Root, diff --git a/src/S7CommPlusDriver/ClientApi/ItemAddress.cs b/src/S7CommPlusDriver/ClientApi/ItemAddress.cs index 53533be..f3ced2f 100644 --- a/src/S7CommPlusDriver/ClientApi/ItemAddress.cs +++ b/src/S7CommPlusDriver/ClientApi/ItemAddress.cs @@ -40,14 +40,26 @@ public ItemAddress(UInt32 area, UInt32 subArea) public ItemAddress(string variableAccessString) { + if (string.IsNullOrWhiteSpace(variableAccessString)) + { + throw new ArgumentException("Variable access string is required.", nameof(variableAccessString)); + } + // Uses a complete access string consisting of hexadecimal strings separated by a dot ("."). // Returns a list of the extracted IDs, e.g. 8A0E0001.A or 52.A List ids = new List(); foreach (string p in variableAccessString.Split('.')) { - ids.Add(UInt32.Parse(p, System.Globalization.NumberStyles.HexNumber)); + if (!UInt32.TryParse(p, System.Globalization.NumberStyles.HexNumber, null, out var id)) + { + throw new ArgumentException($"Variable access string contains an invalid hexadecimal field: '{p}'.", nameof(variableAccessString)); + } + ids.Add(id); + } + if (ids.Count < 2) + { + throw new ArgumentException("Variable access string must contain an access area and at least one local ID field.", nameof(variableAccessString)); } - // TODO: Check for an error, number of fields should be at least 2 SymbolCrc = 0; AccessArea = ids[0]; // Set access area diff --git a/src/S7CommPlusDriver/ClientApi/PlcTag.cs b/src/S7CommPlusDriver/ClientApi/PlcTag.cs index 4e83546..3a924cc 100644 --- a/src/S7CommPlusDriver/ClientApi/PlcTag.cs +++ b/src/S7CommPlusDriver/ClientApi/PlcTag.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Text; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; namespace S7CommPlusDriver.ClientApi { @@ -37,19 +38,19 @@ public virtual void ProcessWriteResult(UInt64 error) LastWriteError = error; } - public abstract PValue GetWriteValue(); + internal abstract PValue GetWriteValue(); protected static int CheckErrorAndType(ulong error, object valueObj, Type checkType) { int res; if (error != 0) { - Console.WriteLine("CheckErrorAndType(): error=" + error); + Trace.WriteLine("CheckErrorAndType(): error=" + error); res = -1; } else if (valueObj.GetType() != checkType) { - Console.WriteLine("CheckErrorAndType(): Type of value is not as excpected. Expected: " + checkType + " Received: " + valueObj.GetType() + "."); + Trace.WriteLine("CheckErrorAndType(): Type of value is not as excpected. Expected: " + checkType + " Received: " + valueObj.GetType() + "."); res = -1; } else @@ -119,7 +120,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueBool(Value); } @@ -156,7 +157,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueByte(Value); } @@ -175,9 +176,18 @@ public class PlcTagChar : PlcTag public char Value { get { return m_Value; } - set { m_Value = value; } // TODO: check if fits in ASCII area, include the encoding? + set + { + var encoding = Encoding.GetEncoding(m_Encoding); + var encoded = encoding.GetBytes(new[] { value }); + if (encoded.Length != 1 || encoding.GetString(encoded)[0] != value) + { + throw new ArgumentOutOfRangeException(nameof(value), $"Character must fit in one byte of {m_Encoding}."); + } + m_Value = value; + } } - + public PlcTagChar(string name, ItemAddress address, uint softdatatype) : base(name, address, softdatatype) { } public override void ProcessReadResult(object valueObj, ulong error) @@ -196,7 +206,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { var c = new char[1]; c[0] = Value; @@ -224,7 +234,7 @@ public ushort Value { get { return m_Value; } set { m_Value = value; } - } + } public PlcTagWord(string name, ItemAddress address, uint softdatatype) : base(name, address, softdatatype) { } @@ -242,7 +252,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueWord(Value); } @@ -261,7 +271,7 @@ public short Value { get { return m_Value; } set { m_Value = value; } - } + } public PlcTagInt(string name, ItemAddress address, uint softdatatype) : base(name, address, softdatatype) { } @@ -279,7 +289,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueInt(Value); } @@ -298,7 +308,7 @@ public uint Value { get { return m_Value; } set { m_Value = value; } - } + } public PlcTagDWord(string name, ItemAddress address, uint softdatatype) : base(name, address, softdatatype) { } @@ -316,7 +326,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueDWord(Value); } @@ -353,7 +363,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueDInt(Value); } @@ -390,7 +400,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueReal(Value); } @@ -404,8 +414,7 @@ public override string ToString() public class PlcTagDate : PlcTag { // Specifies the number of days from January 1, 1990. - // .Net has no type with only date or only time - // TODO: Switch to .Net 6 (for DateOnly) or stay just as UInt? + // Public API stays on DateTime to keep all target frameworks/source users compatible. private DateTime m_Value; public DateTime Value @@ -449,7 +458,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { var dtbase = new DateTime(1990, 1, 1); return new ValueUInt((ushort)(Value - dtbase).Days); @@ -463,7 +472,6 @@ public override string ToString() public class PlcTagTimeOfDay : PlcTag { - // TODO: .Net has no type with only date or only time // Specification: 01:02:03 = 3723000 number of milliseconds since 00:00:00 private uint m_Value; @@ -478,7 +486,7 @@ public uint Value } set - { + { if (value < 86400000) { m_Value = value; @@ -507,7 +515,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueUDInt(Value); } @@ -548,7 +556,7 @@ public int Value get { return m_Value; } set { m_Value = value; } } - + public PlcTagTime(string name, ItemAddress address, uint softdatatype) : base(name, address, softdatatype) { } public override void ProcessReadResult(object valueObj, ulong error) @@ -565,7 +573,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueDInt(Value); } @@ -630,7 +638,7 @@ public class PlcTagS5Time : PlcTag public ushort TimeValue { get - { + { return m_TimeValue; } set @@ -685,7 +693,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { ushort v; v = UshortToBcdUshort(TimeValue); @@ -785,7 +793,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { int[] ts = new int[8]; byte[] b = new byte[8]; @@ -876,7 +884,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { // Must write the complete array of MaxLength of the string (plus two bytes header). byte[] sb = Encoding.GetEncoding(m_Encoding).GetBytes(Value); @@ -931,7 +939,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueUSIntArray(Value); } @@ -954,7 +962,7 @@ public byte[] Value { get { return m_Value; } set { m_Value = value; } - } + } public PlcTagAny(string name, ItemAddress address, uint softdatatype) : base(name, address, softdatatype) { @@ -976,7 +984,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueUSIntArray(Value); } @@ -1021,7 +1029,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueLReal(Value); } @@ -1058,7 +1066,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueULInt(Value); } @@ -1095,7 +1103,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueLInt(Value); } @@ -1132,7 +1140,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueLWord(Value); } @@ -1169,7 +1177,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueUSInt(Value); } @@ -1206,7 +1214,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueUInt(Value); } @@ -1243,7 +1251,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueUDInt(Value); } @@ -1280,7 +1288,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueSInt(Value); } @@ -1317,7 +1325,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueUInt(Convert.ToUInt16(Value)); } @@ -1327,7 +1335,7 @@ public override string ToString() return ResultString(this, Value.ToString()); } } - + public class PlcTagWString : PlcTag { private string m_Value; @@ -1378,7 +1386,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { // Must write the complete array of MaxLength of the string (plus two ushort for the header). var b = new ushort[Value.Length + 2]; @@ -1423,7 +1431,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueTimespan(Value); } @@ -1441,7 +1449,7 @@ public override string ToString() public class PlcTagLTOD : PlcTag { - // TODO: Like the 32 Bit Types Date/TOD, in .Net there's no type for date / time only. Only in .Net 6. + // Public API stays on UInt64 nanoseconds for source compatibility across all target frameworks. // Specification: Number of nanoseconds since 00:00:00. private ulong m_Value; @@ -1485,7 +1493,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueULInt(Value); } @@ -1540,7 +1548,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueTimestamp(Value); } @@ -1626,7 +1634,7 @@ public override void ProcessReadResult(object valueObj, ulong error) // 6: MINUTE, USInt // 7: SECOND, USInt // 8, 9, 10, 11: NANOSECOND, UDInt - + // Use the default timestamp, or refresh it from browsing the plc, or from reading dtl first DTLInterfaceTimestamp = struct_val.PackedStructInterfaceTimestamp; @@ -1642,8 +1650,8 @@ public override void ProcessReadResult(object valueObj, ulong error) Quality = PlcTagQC.TAG_QUALITY_GOOD; } else - { - Quality = PlcTagQC.TAG_QUALITY_BAD; + { + Quality = PlcTagQC.TAG_QUALITY_BAD; } } else @@ -1657,7 +1665,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { var struct_val = new ValueStruct(0x02000043); // 0x02000043 = TI_LIB.SimpleType.67 -> DTL Systemdatatype struct_val.PackedStructInterfaceTimestamp = DTLInterfaceTimestamp; @@ -1736,7 +1744,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueBoolArray(Value); } @@ -1778,7 +1786,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueByteArray(Value); } @@ -1820,7 +1828,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueWordArray(Value); } @@ -1862,7 +1870,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueIntArray(Value); } @@ -1904,7 +1912,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueDWordArray(Value); } @@ -1946,7 +1954,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueDIntArray(Value); } @@ -1988,7 +1996,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueRealArray(Value); } @@ -2030,7 +2038,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueUSIntArray(Value); } @@ -2072,7 +2080,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueUIntArray(Value); } @@ -2114,7 +2122,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueUDIntArray(Value); } @@ -2156,7 +2164,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { return new ValueSIntArray(Value); } @@ -2253,7 +2261,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { var byteStrings = new List(); foreach (var item in Value) @@ -2374,7 +2382,7 @@ public override void ProcessReadResult(object valueObj, ulong error) } } - public override PValue GetWriteValue() + internal override PValue GetWriteValue() { var byteStrings = new List(); foreach (var item in Value) @@ -2410,4 +2418,4 @@ public override string ToString() } } #endregion -} \ No newline at end of file +} diff --git a/src/S7CommPlusDriver/ClientApi/PlcTags.cs b/src/S7CommPlusDriver/ClientApi/PlcTags.cs index 6a43eb0..9fdff95 100644 --- a/src/S7CommPlusDriver/ClientApi/PlcTags.cs +++ b/src/S7CommPlusDriver/ClientApi/PlcTags.cs @@ -1,14 +1,13 @@ -using System; +using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; + +using System.Diagnostics; namespace S7CommPlusDriver.ClientApi { - public static class PlcTags + internal static class PlcTags { - public static int ReadTags(this S7CommPlusConnection conn, IEnumerable plcTags) + public static int ReadTags(this S7CommPlusProtocolSession conn, IEnumerable plcTags) { var readlist = new List(); List values; @@ -33,12 +32,12 @@ public static int ReadTags(this S7CommPlusConnection conn, IEnumerable p } else { - Console.WriteLine("ReadTags: Error res=" + res); + Trace.WriteLine("ReadTags: Error res=" + res); } return res; } - public static int WriteTags(this S7CommPlusConnection conn, IEnumerable plcTags) + public static int WriteTags(this S7CommPlusProtocolSession conn, IEnumerable plcTags) { var writelist = new List(); var values = new List(); @@ -64,7 +63,7 @@ public static int WriteTags(this S7CommPlusConnection conn, IEnumerable } else { - Console.WriteLine("WriteTags: Error res=" + res); + Trace.WriteLine("WriteTags: Error res=" + res); } return res; } @@ -271,7 +270,7 @@ public static PlcTag TagFactory(string name, ItemAddress address, uint softdatat return new PlcTagUInt(name, address, softdatatype); default: - Console.WriteLine("ERROR: Unknown softdatatype=" + softdatatype.ToString() + " for variable= " + name); + Trace.WriteLine("ERROR: Unknown softdatatype=" + softdatatype.ToString() + " for variable= " + name); return null; } } diff --git a/src/S7CommPlusDriver/Core/BlobDecompressor.cs b/src/S7CommPlusDriver/Core/BlobDecompressor.cs index 9c89e52..487ef17 100644 --- a/src/S7CommPlusDriver/Core/BlobDecompressor.cs +++ b/src/S7CommPlusDriver/Core/BlobDecompressor.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -15,10 +15,11 @@ using ComponentAce.Compression.Libs.zlib; using System; +using System.IO; namespace S7CommPlusDriver { - public class BlobDecompressor + internal class BlobDecompressor { /// /// Decompresses a zlib compressed blob. @@ -29,6 +30,15 @@ public class BlobDecompressor /// The decompressed blob as string. If decompression failed, the string is empty public string decompress(byte[] compressed_blob, int startoffset) { + if (compressed_blob == null) + { + throw new ArgumentNullException(nameof(compressed_blob)); + } + if (startoffset < 0 || startoffset >= compressed_blob.Length) + { + throw new ArgumentOutOfRangeException(nameof(startoffset)); + } + int retcode; string retstring = String.Empty; byte[] dict = null; @@ -38,7 +48,7 @@ public string decompress(byte[] compressed_blob, int startoffset) ZStream z = new ZStream(); z.inflateInit(); - z.avail_in = compressed_blob.Length; + z.avail_in = compressed_blob.Length - startoffset; z.next_in = compressed_blob; z.next_in_index = startoffset; z.next_out = uncompressed_blob; @@ -109,7 +119,7 @@ public string decompress(byte[] compressed_blob, int startoffset) dict = dict_CompilerSettings_90000001; break; default: - Console.WriteLine(String.Format("BlobDecompressor: Have no dictionary for adler: 0x{0:X}", z.adler)); + System.Diagnostics.Trace.WriteLine(String.Format("BlobDecompressor: Have no dictionary for adler: 0x{0:X}", z.adler)); break; } if (dict != null) @@ -143,24 +153,27 @@ public string decompress(byte[] compressed_blob, int startoffset) } retcode = z.inflate(zlibConst.Z_FINISH); } - if (retcode == zlibConst.Z_STREAM_END) + int producedLength = checked((int)z.total_out); + if (retcode == zlibConst.Z_STREAM_END || (retcode == zlibConst.Z_BUF_ERROR && producedLength > 0)) { - uncomp_length -= z.avail_out; - - if (uncomp_length > 0) + if (producedLength > 0) { if (z.avail_out == 0) { // need one more byte for string null terminator - Array.Resize(ref uncompressed_blob, uncomp_length + 1); + Array.Resize(ref uncompressed_blob, producedLength + 1); } - uncompressed_blob[uncomp_length] = 0x00; + uncompressed_blob[producedLength] = 0x00; + } + if (retcode == zlibConst.Z_BUF_ERROR) + { + System.Diagnostics.Trace.WriteLine(String.Format("BlobDecompressor: Blob ended without a zlib stream trailer; returning {0} decompressed bytes.", producedLength)); } - retstring = System.Text.Encoding.UTF8.GetString(uncompressed_blob, 0, uncomp_length); + retstring = System.Text.Encoding.UTF8.GetString(uncompressed_blob, 0, producedLength); } else { - Console.WriteLine(String.Format("BlobDecompressor: Blob decompression failed! Zlib retcode={0} msg={1} ", retcode, z.msg)); + throw new InvalidDataException(String.Format("Blob decompression failed. Zlib retcode={0} msg={1}.", retcode, z.msg)); } z.inflateEnd(); return retstring; @@ -931,7 +944,7 @@ public string decompress(byte[] compressed_blob, int startoffset) }; #endregion - #region Dictionary IntRefData 98000001 + #region Dictionary IntRefData 98000001 private const uint DICTID_IntRefData_98000001 = 0xb0155ff8; private static readonly byte[] dict_IntRefData_98000001 = new byte[] { diff --git a/src/S7CommPlusDriver/Core/CreateObjectRequest.cs b/src/S7CommPlusDriver/Core/CreateObjectRequest.cs index 936dcd8..94c476d 100644 --- a/src/S7CommPlusDriver/Core/CreateObjectRequest.cs +++ b/src/S7CommPlusDriver/Core/CreateObjectRequest.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -14,13 +14,16 @@ #endregion using System; +using System.Net; using System.IO; +using System.Security.Cryptography; +using S7CommPlusDriver.Internal; namespace S7CommPlusDriver { - public class CreateObjectRequest : IS7pRequest + internal class CreateObjectRequest : IS7pRequest { - public byte TransportFlags = 0x36; + public byte TransportFlags = S7CommPlusProtocolConstants.CreateObjectTransportFlags; public UInt32 RequestId; public PValue RequestValue; public PObject RequestObject; @@ -54,7 +57,7 @@ public void SetNullServerSessionData() { // Initializes the data for a Nullserver Session on connection setup. // SessionId is set automatically to Ids.ObjectNullServerSession when this object is sent, if there's no session Id. - TransportFlags = 0x36; + TransportFlags = S7CommPlusProtocolConstants.CreateObjectTransportFlags; RequestId = Ids.ObjectServerSessionContainer; RequestValue = new ValueUDInt(0); @@ -63,6 +66,59 @@ public void SetNullServerSessionData() RequestObject.AddObject(new PObject(RID: Ids.GetNewRIDOnServer, CLSID: Ids.ClassSubscriptions, AID: Ids.None)); } + public void SetTiaServerSessionData(LegacyServerSessionRole role) + { + TransportFlags = S7CommPlusProtocolConstants.CreateObjectTransportFlags; + RequestId = Ids.ObjectServerSessionContainer; + RequestValue = new ValueUDInt(0); + + var clientRid = CreateClientRid(); + var host = GetHostName(); + var user = GetUserName(); + var clientName = $"{host}_{clientRid:X8}_1600.1.115.1"; + + RequestObject = new PObject(RID: Ids.GetNewRIDOnServer, CLSID: Ids.ClassServerSession, AID: Ids.None); + RequestObject.AddAttribute(Ids.ObjectVariableTypeName, new ValueWString(clientName)); + RequestObject.AddAttribute(Ids.ServerSessionClientConnectionId, new ValueWString("1:::6.0::S7CommPlusDriver.TCPIP.1")); + RequestObject.AddAttribute(Ids.ServerSessionUser, new ValueWString(user)); + RequestObject.AddAttribute(Ids.ServerSessionApplication, new ValueWString(string.Empty)); + RequestObject.AddAttribute(Ids.ServerSessionHost, new ValueWString(host)); + RequestObject.AddAttribute(Ids.ServerSessionRole, new ValueUDInt((uint)role)); + RequestObject.AddAttribute(Ids.ServerSessionClientRID, new ValueRID(clientRid)); + RequestObject.AddAttribute(Ids.ServerSessionComment, new ValueWString(string.Empty)); + + var subscriptions = new PObject(RID: Ids.GetNewRIDOnServer, CLSID: Ids.ClassSubscriptions, AID: Ids.None); + subscriptions.AddAttribute(Ids.ObjectVariableTypeName, new ValueWString("SubscriptionContainer")); + RequestObject.AddObject(subscriptions); + } + + private static uint CreateClientRid() + { + Span bytes = stackalloc byte[4]; + RandomNumberGenerator.Fill(bytes); + var value = BitConverter.ToUInt32(bytes); + return value == 0 ? 1u : value; + } + + private static string GetHostName() + { + try + { + var host = Dns.GetHostName(); + return string.IsNullOrWhiteSpace(host) ? "S7CommPlusDriver" : host; + } + catch + { + return "S7CommPlusDriver"; + } + } + + private static string GetUserName() + { + var user = Environment.UserName; + return string.IsNullOrWhiteSpace(user) ? "---" : user; + } + public byte GetProtocolVersion() { return ProtocolVersion; diff --git a/src/S7CommPlusDriver/Core/CreateObjectResponse.cs b/src/S7CommPlusDriver/Core/CreateObjectResponse.cs index 0b924bc..cac7c89 100644 --- a/src/S7CommPlusDriver/Core/CreateObjectResponse.cs +++ b/src/S7CommPlusDriver/Core/CreateObjectResponse.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -19,7 +19,7 @@ namespace S7CommPlusDriver { - public class CreateObjectResponse : IS7pResponse + internal class CreateObjectResponse : IS7pResponse { public byte TransportFlags; public UInt64 ReturnValue; diff --git a/src/S7CommPlusDriver/Core/DeleteObjectRequest.cs b/src/S7CommPlusDriver/Core/DeleteObjectRequest.cs index 0d45a9c..c98100a 100644 --- a/src/S7CommPlusDriver/Core/DeleteObjectRequest.cs +++ b/src/S7CommPlusDriver/Core/DeleteObjectRequest.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -15,12 +15,13 @@ using System; using System.IO; +using S7CommPlusDriver.Internal; namespace S7CommPlusDriver { - public class DeleteObjectRequest : IS7pRequest + internal class DeleteObjectRequest : IS7pRequest { - byte TransportFlags = 0x34; + byte TransportFlags = S7CommPlusProtocolConstants.RequestWithResponseTransportFlags; public UInt32 DeleteObjectId; public uint SessionId { get; set; } diff --git a/src/S7CommPlusDriver/Core/DeleteObjectResponse.cs b/src/S7CommPlusDriver/Core/DeleteObjectResponse.cs index d62594b..7f1e6ed 100644 --- a/src/S7CommPlusDriver/Core/DeleteObjectResponse.cs +++ b/src/S7CommPlusDriver/Core/DeleteObjectResponse.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -18,7 +18,7 @@ namespace S7CommPlusDriver { - public class DeleteObjectResponse : IS7pResponse + internal class DeleteObjectResponse : IS7pResponse { public byte TransportFlags; public UInt64 ReturnValue; diff --git a/src/S7CommPlusDriver/Core/ExploreRequest.cs b/src/S7CommPlusDriver/Core/ExploreRequest.cs index b4c9737..3d9dc1c 100644 --- a/src/S7CommPlusDriver/Core/ExploreRequest.cs +++ b/src/S7CommPlusDriver/Core/ExploreRequest.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -16,12 +16,13 @@ using System; using System.Collections.Generic; using System.IO; +using S7CommPlusDriver.Internal; namespace S7CommPlusDriver { - public class ExploreRequest : IS7pRequest + internal class ExploreRequest : IS7pRequest { - byte TransportFlags = 0x34; // or 0x36??? + byte TransportFlags = S7CommPlusProtocolConstants.RequestWithResponseTransportFlags; public UInt32 ExploreId; public UInt32 ExploreRequestId; public byte ExploreChildsRecursive; @@ -69,7 +70,7 @@ public int Serialize(Stream buffer) { ret += S7p.EncodeByte(buffer, 1); // 1 object / value - // TODO / Experimental: + // Experimental compatibility path for older firmware/protocol captures: // Not 100% sure about how this has to be used: // On a Struct, we don't write the datatypeflags into the stream. // Maybe the byte before are the flags (which is the way I have it in the Wireshark dissector so far, which may be wrong). diff --git a/src/S7CommPlusDriver/Core/ExploreResponse.cs b/src/S7CommPlusDriver/Core/ExploreResponse.cs index a1b20d8..94cb80f 100644 --- a/src/S7CommPlusDriver/Core/ExploreResponse.cs +++ b/src/S7CommPlusDriver/Core/ExploreResponse.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -19,7 +19,7 @@ namespace S7CommPlusDriver { - public class ExploreResponse : IS7pResponse + internal class ExploreResponse : IS7pResponse { public byte TransportFlags; public UInt64 ReturnValue; diff --git a/src/S7CommPlusDriver/Core/GetMultiVariablesRequest.cs b/src/S7CommPlusDriver/Core/GetMultiVariablesRequest.cs index 7fa0ed8..59f4a56 100644 --- a/src/S7CommPlusDriver/Core/GetMultiVariablesRequest.cs +++ b/src/S7CommPlusDriver/Core/GetMultiVariablesRequest.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -16,12 +16,13 @@ using System; using System.Collections.Generic; using System.IO; +using S7CommPlusDriver.Internal; namespace S7CommPlusDriver { class GetMultiVariablesRequest : IS7pRequest { - byte TransportFlags = 0x34; + byte TransportFlags = S7CommPlusProtocolConstants.RequestWithResponseTransportFlags; public UInt32 LinkId = 0; // for reading variables, this should be 0 public List AddressList = new List(); diff --git a/src/S7CommPlusDriver/Core/GetMultiVariablesResponse.cs b/src/S7CommPlusDriver/Core/GetMultiVariablesResponse.cs index ed95ef5..6e9bab6 100644 --- a/src/S7CommPlusDriver/Core/GetMultiVariablesResponse.cs +++ b/src/S7CommPlusDriver/Core/GetMultiVariablesResponse.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -19,7 +19,7 @@ namespace S7CommPlusDriver { - public class GetMultiVariablesResponse : IS7pResponse + internal class GetMultiVariablesResponse : IS7pResponse { public byte TransportFlags; public UInt64 ReturnValue; diff --git a/src/S7CommPlusDriver/Core/GetVarSubstreamedRequest.cs b/src/S7CommPlusDriver/Core/GetVarSubstreamedRequest.cs index 208e173..ff1a387 100644 --- a/src/S7CommPlusDriver/Core/GetVarSubstreamedRequest.cs +++ b/src/S7CommPlusDriver/Core/GetVarSubstreamedRequest.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -15,12 +15,13 @@ using System; using System.IO; +using S7CommPlusDriver.Internal; namespace S7CommPlusDriver { - public class GetVarSubstreamedRequest : IS7pRequest + internal class GetVarSubstreamedRequest : IS7pRequest { - public byte TransportFlags = 0x34; + public byte TransportFlags = S7CommPlusProtocolConstants.RequestWithResponseTransportFlags; public UInt32 InObjectId; @@ -57,14 +58,15 @@ public int Serialize(Stream buffer) // Request set ret += S7p.EncodeUInt32(buffer, InObjectId); - ret += S7p.EncodeByte(buffer, 0x20); // Addressarray + ret += S7p.EncodeByte(buffer, S7CommPlusProtocolConstants.ValueAddressArrayFlag); ret += S7p.EncodeByte(buffer, Datatype.UDInt); ret += S7p.EncodeByte(buffer, 1); // Array size - ret += S7p.EncodeUInt32Vlq(buffer, Address); - + ret += S7p.EncodeUInt32Vlq(buffer, Address); + ret += S7p.EncodeObjectQualifier(buffer); - // 2 Bytes unknown - ret += S7p.EncodeUInt16(buffer, 0x0001); + // 2 Bytes unknown, decoded by the Wireshark S7CommPlus dissector as + // getvarsubstr.req_unknown1. Captured legacy V3 traffic uses 0x0001. + ret += S7p.EncodeUInt16(buffer, S7CommPlusProtocolConstants.GetVarSubstreamedRequestUnknown1); if (WithIntegrityId) { @@ -73,6 +75,10 @@ public int Serialize(Stream buffer) // Fill? ret += S7p.EncodeUInt32(buffer, 0); + if (ProtocolVersion == S7CommPlusDriver.ProtocolVersion.V3) + { + ret += S7p.EncodeByte(buffer, 0); + } return ret; } diff --git a/src/S7CommPlusDriver/Core/GetVarSubstreamedResponse.cs b/src/S7CommPlusDriver/Core/GetVarSubstreamedResponse.cs index d9bbb61..7c40723 100644 --- a/src/S7CommPlusDriver/Core/GetVarSubstreamedResponse.cs +++ b/src/S7CommPlusDriver/Core/GetVarSubstreamedResponse.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -18,7 +18,7 @@ namespace S7CommPlusDriver { - public class GetVarSubstreamedResponse : IS7pResponse + internal class GetVarSubstreamedResponse : IS7pResponse { public byte TransportFlags; public UInt64 ReturnValue; diff --git a/src/S7CommPlusDriver/Core/GetVariableRequest.cs b/src/S7CommPlusDriver/Core/GetVariableRequest.cs new file mode 100644 index 0000000..4e965f7 --- /dev/null +++ b/src/S7CommPlusDriver/Core/GetVariableRequest.cs @@ -0,0 +1,94 @@ +#region License +/****************************************************************************** + * S7CommPlusDriver + * + * Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de + * + * This file is part of S7CommPlusDriver. + * + * S7CommPlusDriver is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + /****************************************************************************/ +#endregion + +using System; +using System.IO; +using S7CommPlusDriver.Internal; + +namespace S7CommPlusDriver +{ + internal class GetVariableRequest : IS7pRequest + { + public byte TransportFlags = S7CommPlusProtocolConstants.RequestWithResponseTransportFlags; + + public UInt32 InObjectId; + + public UInt32 Address; + + public uint SessionId { get; set; } + public byte ProtocolVersion { get; set; } + public ushort FunctionCode { get => Functioncode.GetVariable; } + public ushort SequenceNumber { get; set; } + public uint IntegrityId { get; set; } + public bool WithIntegrityId { get; set; } + + public GetVariableRequest(byte protocolVersion) + { + ProtocolVersion = protocolVersion; + WithIntegrityId = true; + } + + public byte GetProtocolVersion() + { + return ProtocolVersion; + } + + public int Serialize(Stream buffer) + { + int ret = 0; + ret += S7p.EncodeByte(buffer, Opcode.Request); + ret += S7p.EncodeUInt16(buffer, 0); + ret += S7p.EncodeUInt16(buffer, FunctionCode); + ret += S7p.EncodeUInt16(buffer, 0); + ret += S7p.EncodeUInt16(buffer, SequenceNumber); + ret += S7p.EncodeUInt32(buffer, SessionId); + ret += S7p.EncodeByte(buffer, TransportFlags); + + ret += S7p.EncodeUInt32(buffer, InObjectId); + ret += S7p.EncodeUInt32Vlq(buffer, 1); + ret += S7p.EncodeUInt32Vlq(buffer, Address); + + ret += S7p.EncodeObjectQualifier(buffer); + ret += S7p.EncodeByte(buffer, 0x00); + + if (WithIntegrityId) + { + ret += S7p.EncodeUInt32Vlq(buffer, IntegrityId); + } + + ret += S7p.EncodeUInt32(buffer, 0); + + return ret; + } + + public override string ToString() + { + string s = ""; + s += "" + Environment.NewLine; + s += "" + ProtocolVersion.ToString() + "" + Environment.NewLine; + s += "" + SequenceNumber.ToString() + "" + Environment.NewLine; + s += "" + SessionId.ToString() + "" + Environment.NewLine; + s += "" + TransportFlags.ToString() + "" + Environment.NewLine; + s += "" + Environment.NewLine; + s += "" + InObjectId.ToString() + "" + Environment.NewLine; + s += "" + Environment.NewLine; + s += "" + Address.ToString() + "" + Environment.NewLine; + s += "" + Environment.NewLine; + s += "" + Environment.NewLine; + s += "" + Environment.NewLine; + return s; + } + } +} diff --git a/src/S7CommPlusDriver/Core/GetVariableResponse.cs b/src/S7CommPlusDriver/Core/GetVariableResponse.cs new file mode 100644 index 0000000..ea434c2 --- /dev/null +++ b/src/S7CommPlusDriver/Core/GetVariableResponse.cs @@ -0,0 +1,124 @@ +#region License +/****************************************************************************** + * S7CommPlusDriver + * + * Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de + * + * This file is part of S7CommPlusDriver. + * + * S7CommPlusDriver is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + /****************************************************************************/ +#endregion + +using System; +using System.IO; + +namespace S7CommPlusDriver +{ + internal class GetVariableResponse : IS7pResponse + { + public byte TransportFlags; + public UInt64 ReturnValue; + public PValue Value; + + public byte ProtocolVersion { get; set; } + public ushort FunctionCode { get => Functioncode.GetVariable; } + public ushort SequenceNumber { get; set; } + public uint IntegrityId { get; set; } + public bool WithIntegrityId { get; set; } + + public GetVariableResponse(byte protocolVersion) + { + ProtocolVersion = protocolVersion; + WithIntegrityId = true; + } + + public int Deserialize(Stream buffer) + { + int ret = 0; + + ret += S7p.DecodeUInt16(buffer, out ushort seqnr); + SequenceNumber = seqnr; + ret += S7p.DecodeByte(buffer, out TransportFlags); + + ret += S7p.DecodeUInt64Vlq(buffer, out ReturnValue); + + var valueStart = buffer.Position; + if (!TryDeserializeValue(buffer, out Value)) + { + buffer.Position = valueStart; + S7p.DecodeByte(buffer, out _); + ret++; + if (!TryDeserializeValue(buffer, out Value)) + { + Value = null; + } + } + + ret += S7p.DecodeUInt32Vlq(buffer, out uint iid); + IntegrityId = iid; + return ret; + } + + private static bool TryDeserializeValue(Stream buffer, out PValue value) + { + value = null; + var start = buffer.Position; + try + { + value = PValue.Deserialize(buffer); + return value != null; + } + catch + { + buffer.Position = start; + return false; + } + } + + public override string ToString() + { + string s = ""; + s += "" + Environment.NewLine; + s += "" + ProtocolVersion.ToString() + "" + Environment.NewLine; + s += "" + SequenceNumber.ToString() + "" + Environment.NewLine; + s += "" + TransportFlags.ToString() + "" + Environment.NewLine; + s += "" + Environment.NewLine; + s += "" + ReturnValue.ToString() + "" + Environment.NewLine; + if (Value != null) + { + s += Value.ToString() + Environment.NewLine; + } + s += "" + Environment.NewLine; + s += "" + IntegrityId.ToString() + "" + Environment.NewLine; + s += "" + Environment.NewLine; + return s; + } + + public static GetVariableResponse DeserializeFromPdu(Stream pdu) + { + S7p.DecodeByte(pdu, out byte protocolVersion); + S7p.DecodeByte(pdu, out byte opcode); + if (opcode != Opcode.Response) + { + return null; + } + + S7p.DecodeUInt16(pdu, out _); + S7p.DecodeUInt16(pdu, out ushort function); + S7p.DecodeUInt16(pdu, out _); + if (function != Functioncode.GetVariable) + { + return null; + } + + GetVariableResponse resp = new GetVariableResponse(protocolVersion); + resp.Deserialize(pdu); + + return resp; + } + } +} diff --git a/src/S7CommPlusDriver/Core/IS7pRequest.cs b/src/S7CommPlusDriver/Core/IS7pRequest.cs index 00c06a8..2fa41da 100644 --- a/src/S7CommPlusDriver/Core/IS7pRequest.cs +++ b/src/S7CommPlusDriver/Core/IS7pRequest.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -15,7 +15,7 @@ namespace S7CommPlusDriver { - public interface IS7pRequest + internal interface IS7pRequest { int Serialize(System.IO.Stream buffer); diff --git a/src/S7CommPlusDriver/Core/IS7pResponse.cs b/src/S7CommPlusDriver/Core/IS7pResponse.cs index 5c34528..a606007 100644 --- a/src/S7CommPlusDriver/Core/IS7pResponse.cs +++ b/src/S7CommPlusDriver/Core/IS7pResponse.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -15,7 +15,7 @@ namespace S7CommPlusDriver { - public interface IS7pResponse + internal interface IS7pResponse { byte ProtocolVersion { diff --git a/src/S7CommPlusDriver/Core/Ids.cs b/src/S7CommPlusDriver/Core/Ids.cs index 12b330e..67c6b80 100644 --- a/src/S7CommPlusDriver/Core/Ids.cs +++ b/src/S7CommPlusDriver/Core/Ids.cs @@ -18,14 +18,24 @@ namespace S7CommPlusDriver public static class Ids { public const int None = 0; + public const int NativeObjects_theASRoot_Rid = 1; + public const int NativeObjects_theHardwareConfiguration_Rid = 2; public const int NativeObjects_thePLCProgram_Rid = 3; + public const int PlcProgram = NativeObjects_thePLCProgram_Rid; public const int NativeObjects_theAlarmSubsystem_Rid = 8; + public const int AlarmSubSystem = NativeObjects_theAlarmSubsystem_Rid; + public const int NativeObjects_theCPU_Rid = 48; + public const int NativeObjects_theCPUProxy_Rid = 49; + public const int NativeObjects_theCPUCommon_Rid = 50; public const int NativeObjects_theCPUexecUnit_Rid = 52; + public const int NativeObjects_theTextContainer_Rid = 114; public const int NativeObjects_theIArea_Rid = 80; public const int NativeObjects_theQArea_Rid = 81; public const int NativeObjects_theMArea_Rid = 82; public const int NativeObjects_theS7Counters_Rid = 83; public const int NativeObjects_theS7Timers_Rid = 84; + public const int NativeObjects_thePKIContainer_Rid = 115; + public const int NativeObjects_theAdaptedPKIStore_Rid = 145; public const int ObjectRoot = 201; public const int GetNewRIDOnServer = 211; public const int ObjectVariableTypeParentObject = 229; @@ -35,25 +45,69 @@ public static class Ids public const int ObjectServerSessionContainer = 285; public const int ClassServerSession = 287; public const int ObjectNullServerSession = 288; + public const int ServerSessionClientConnectionId = 289; + public const int ServerSessionUser = 296; + public const int ServerSessionApplication = 297; + public const int ServerSessionHost = 298; + public const int ServerSessionRole = 299; public const int ServerSessionClientRID = 300; + public const int ServerSessionComment = 301; public const int ServerSessionRequest = 303; + public const int ServerSessionChallenge = ServerSessionRequest; + public const int SessionServerChallenge = ServerSessionChallenge; public const int ServerSessionResponse = 304; + public const int ServerSessionRoles = 305; public const int ServerSessionVersion = 306; + public const int ServerSessionProgressOms = 307; + public const int ServerSessionProgressBl = 308; + public const int ClientSessionLegitimated = 310; + public const int LID_SessionVersionStruct = 314; + public const int LID_SessionVersionSystemOMS = 315; + public const int LID_SessionVersionProjectOMS = 316; + public const int LID_SessionVersionSystemPAOM = 317; + public const int LID_SessionVersionProjectPAOM = 318; public const int LID_SessionVersionSystemPAOMString = 319; + public const int LID_SessionVersionProjectPAOMString = 320; + public const int LID_SessionVersionProjectFormat = 321; public const int ClassTypeInfo = 511; public const int ClassOMSTypeInfoContainer = 534; public const int ObjectOMSTypeInfoContainer = 537; public const int TextLibraryClassRID = 606; public const int TextLibraryOffsetArea = 608; public const int TextLibraryStringArea = 609; + public const int TextContainer_Class_Rid = 4672; + public const int TextContainer_LCIDs_Aid = 2717; public const int ClassSubscription = 1001; public const int SubscriptionMissedSendings = 1002; public const int SubscriptionSubsystemError = 1003; public const int SubscriptionReferenceTriggerAndTransmitMode = 1005; + public const int SubscriptionReferenceMode = SubscriptionReferenceTriggerAndTransmitMode; public const int SystemLimits = 1037; public const int SubscriptionRouteMode = 1040; public const int SubscriptionActive = 1041; public const int Legitimate = 1846; + public const int LegacySessionSecretResponse = Legitimate; + public const int StructSecurityKey = 1800; + public const int SecurityKeyVersion = 1801; + public const int SecurityKeySecurityLevel = 1802; + public const int SecurityKeyPublicKeyID = 1803; + public const int SecurityKeyPublicKeyId = SecurityKeyPublicKeyID; + public const int SecurityKeySymmetricKeyID = 1804; + public const int SecurityKeySymmetricKeyId = SecurityKeySymmetricKeyID; + public const int SecurityKeyEncryptedKey = 1805; + public const int EncryptionData = 1811; + public const int StructMAC = 1820; + public const int MACAlgorithm = 1821; + public const int MACEncryptedKey = 1822; + public const int MACData = 1823; + public const int SecurityKeyID = 1825; + public const int SecurityKeyId = SecurityKeyID; + public const int SecurityKeyIdValue = 1826; + public const int SecurityKeyIdFlags = 1827; + public const int SecurityKeyIdInternalFlags = 1828; + public const int ServerSessionSessionKey = 1830; + public const int SessionKey = ServerSessionSessionKey; + public const int LegacyAuthenticationCompatibilityFlag = 1902; public const int SubscriptionReferenceList = 1048; public const int SubscriptionCycleTime = 1049; public const int SubscriptionDelayTime = 1050; @@ -74,49 +128,174 @@ public static class Ids public const int KeyQualifier = 1259; public const int TI_TComSize = 1502; public const int EffectiveProtectionLevel = 1842; + public const int CurrentPlcSecurityLevel = EffectiveProtectionLevel; public const int ActiveProtectionLevel = 1843; + public const int ExpectedLegitimationLevel = 1844; + public const int CollaborationToken = 1845; public const int CPUexecUnit_operatingStateReq = 2167; + public const int CPUexecUnit_ConfiguredMaxScanCycle = 2171; + public const int CPUexecUnit_ConfiguredMinScanCycle = 2172; + public const int CPUexecUnit_LongestScanCycle = 2173; + public const int CPUexecUnit_ShortestScanCycle = 2174; + public const int CPUexecUnit_CurrentScanCycle = 2175; + public const int HWObject_DIS = 2237; + public const int CPUCommon_WorkMemoryTotal = 2343; + public const int CPUCommon_WorkMemoryUsed = 2344; + public const int CPUCommon_RetainMemoryTotal = 2416; + public const int CPUCommon_RetainMemoryUsed = 2417; + public const int CPUCommon_LoadMemoryTotal = 2418; + public const int CPUCommon_LoadMemoryUsed = 2419; + public const int AS_DIS_OperatingState = 3486; + public const int CPUCommon_OperatingState = 3451; + public const int CPUCommon_StateSwitch = 3701; + public const int CPUCommon_WorkMemoryCodeTotal = 4554; + public const int CPUCommon_WorkMemoryDataTotal = 4555; + public const int CPUCommon_WorkMemoryCodeUsed = 4556; + public const int CPUCommon_WorkMemoryDataUsed = 4557; + public const int CPUCommon_StateSwitch2 = 4570; + public const int ASRoot_ItsFolders = 2468; public const int PLCProgram_Class_Rid = 2520; - public const int Block_BlockNumber = 2521; + public const int Block_BlockNumber = 2521; + public const int Block_AutoNumbering = 2522; + public const int Block_BlockLanguage = 2523; + public const int Block_KnowhowProtected = 2524; + public const int Block_Unlinked = 2527; + public const int Block_LowLatency = 2528; + public const int Block_RuntimeModified = 2529; + public const int Block_Dirty = 2531; + public const int Block_CRC = 2532; + public const int Block_Binding = 3151; + public const int Block_OptimizeInfo = 2537; + public const int Block_TOblockSetNumber = 3619; + public const int Block_TypeInfo = 4578; + public const int Block_FunctionalSignature = 7589; + public const int Block_AdditionalMAC = 7831; + public const int Block_FailsafeBlockInfo = 7843; + public const int Block_FailsafeIFRHash = 7955; + public const int ASObjectES_IdentES = 2449; + public const int Block_BodyDescription = 2533; public const int DataInterface_InterfaceDescription = 2544; public const int DataInterface_LineComments = 2546; public const int DB_ValueInitial = 2548; public const int DB_ValueActual = 2550; - public const int DB_InitialChanged = 2551; - public const int DB_Class_Rid = 2574; + public const int DB_InitialChanged = 2551; + public const int DB_Class_Rid = 2574; + public const int FB_Class_Rid = 2578; + public const int FC_Class_Rid = 2579; + public const int FunctionalObject_Code = 2580; + public const int FunctionalObject_ParameterModified = 2581; + public const int FunctionalObject_extRefData = 2582; + public const int FunctionalObject_intRefData = 2583; + public const int FunctionalObject_NetworkComments = 2584; + public const int FunctionalObject_NetworkTitles = 2585; + public const int FunctionalObject_CalleeList = 2586; + public const int FunctionalObject_InterfaceSignature = 2587; + public const int FunctionalObject_DisplayInfo = 2588; + public const int FunctionalObject_DebugInfo = 2589; + public const int FunctionalObject_LocalErrorhandling = 2590; + public const int FunctionalObject_LongConstants = 2591; + public const int FunctionalObject_Class_Rid = 2592; + public const int OB_StartInfoType = 2607; + public const int OB_Class_Rid = 2610; public const int AlarmSubscriptionRef_AlarmDomain = 2659; + public const int AlarmSubSystem_AlarmDomain = AlarmSubscriptionRef_AlarmDomain; public const int AlarmSubscriptionRef_itsAlarmSubsystem = 2660; + public const int AlarmSubsystem_itsAlarmSubscriptionRef = 2661; public const int AlarmSubscriptionRef_Class_Rid = 2662; + public const int AlarmSubScrClass_Identifier = AlarmSubscriptionRef_Class_Rid; + public const int AlarmSubsystem_SmallDAICount = 2663; + public const int AlarmSubsystem_BigDAISize = 2664; + public const int AlarmSubsystem_itsDAI = 2665; + public const int DAI_itsAlarmSubsystem = 2666; public const int AlarmSubsystem_itsUpdateRelevantDAI = 2667; + public const int AlarmSubSystem_Update_DAI = AlarmSubsystem_itsUpdateRelevantDAI; + public const int DAI_itsUpdateRelevantView = 2668; + public const int AlarmSubsystem_Class_Rid = 2669; public const int DAI_CPUAlarmID = 2670; + public const int DynamicAlarmInstance_DAI_AlarmId = DAI_CPUAlarmID; public const int DAI_AllStatesInfo = 2671; + public const int DynamicAlarmInstance_DAI_AllStateInformation = DAI_AllStatesInfo; public const int DAI_AlarmDomain = 2672; + public const int DynamicAlarmInstance_DAI_AlarmDomain = DAI_AlarmDomain; public const int DAI_Coming = 2673; + public const int DynamicAlarmInstance_DAI_Coming = DAI_Coming; public const int DAI_Going = 2677; + public const int DynamicAlarmInstance_DAI_Going = DAI_Going; public const int DAI_Class_Rid = 2681; + public const int DynamicAlarmInstance_DAI_ClassId = DAI_Class_Rid; + public const int AbstractTisJob_TisJobEnabledConf = 2687; + public const int AbstractTisJob_TisJobEnabledActual = 2688; + public const int AbstractTisJob_ContinuingJob = 2689; + public const int AbstractTisJob_CreationTimestamp = 2690; + public const int AbstractTisJob_ModifyingJob = 2691; + public const int AbstractTisJob_NotificationCredit = 2692; + public const int AbstractTisJob_Request = 2693; + public const int AbstractTisJob_Trigger = 2694; + public const int AbstractTisJob_Result = 2695; + public const int AbstractTisJob_itsAssumingSubscriptionRef = 2696; + public const int TisSubscriptionRef_itsAssumingJob = 2697; + public const int AbstractTisJob_itsTisSubsystem = 2698; + public const int AbstractTisJob_Class_Rid = 2700; + public const int TisWatchJob_Class_Rid = 2709; + public const int TisSubscriptionRef_IncrementNotificationCredit = 2711; + public const int TisSubscriptionRef_Class_Rid = 2712; public const int DAI_AlarmTexts_Rid = 2715; + public const int DynamicAlarmInstance_DAI_Alarmtext = DAI_AlarmTexts_Rid; + public const int STAI_itsSDiagCont = 3095; + public const int STAI_Class_Rid = 3096; + public const int STAI_itsDB = 3110; + public const int STAI_AlarmDomain = 3402; + public const int STAI_AlarmEnabled = 3403; + public const int STAI_ALID = 3404; + public const int AS_CGS_Class_Rid = 3473; public const int AS_CGS_AllStatesInfo = 3474; public const int AS_CGS_Timestamp = 3475; public const int AS_CGS_AssociatedValues = 3476; + public const int AbstractTisJob_ModifyingJobChangeCounterCopy = 3626; + public const int AlarmSubsystem_itsEnDisJob = 3644; + public const int AlarmSubsystem_itsAckJob = 3645; public const int AS_CGS_AckTimestamp = 3646; public const int ControllerArea_ValueInitial = 3735; public const int ControllerArea_ValueActual = 3736; public const int ControllerArea_RuntimeModified = 3737; + public const int UDT_Class_Rid = 3932; public const int DAI_MessageType = 4079; + public const int DynamicAlarmInstance_DAI_Messagetype = DAI_MessageType; + public const int AlarmSubsystem_itsDisabledSTAI = 4123; + public const int ConstantsGlobal_Symbolics = 4275; public const int ASObjectES_Comment = 4288; + public const int AlarmSubsystem_itsUpdateRequestToAllJob = 4569; + public const int AbstractTisJob_LargeBufferMemorySize = 7647; public const int AlarmSubscriptionRef_AlarmDomain2 = 7731; + public const int AlarmSubscr_AlarmDomain2 = AlarmSubscriptionRef_AlarmDomain2; public const int DAI_HmiInfo = 7813; + public const int DynamicAlarmInstance_DAI_HMIInformation = DAI_HmiInfo; + public const int STAI_MessageType = 7770; + public const int STAI_HmiInfo = 7771; + public const int STAI_InitFbState = 7772; public const int MultipleSTAI_Class_Rid = 7854; + public const int MultipleSTAI_itsSDiagCont = 7855; + public const int MultipleSTAI_itsDB = 7856; public const int MultipleSTAI_STAIs = 7859; + public const int MultipleSTAI_STAIsSyntaxID = 7904; public const int DAI_SequenceCounter = 7917; - public const int AlarmSubscriptionRef_AlarmTextLanguages_Rid = 8181; + public const int DynamicAlarmInstance_DAI_SequenceCounter = DAI_SequenceCounter; public const int AlarmSubscriptionRef_SendAlarmTexts_Rid = 8173; + public const int AlarmSubscr_SendAlarmTextIdentifier = AlarmSubscriptionRef_SendAlarmTexts_Rid; + public const int AlarmSubscriptionRef_Reserved_Rid = 8181; + public const int AlarmSubscriptionRef_AlarmTextLanguages_Rid = 8181; + public const int AlarmSubscr_AlarmTextLangIdentifier = AlarmSubscriptionRef_AlarmTextLanguages_Rid; + public const int AlarmSubsystem_Reserved = 8182; public const int ReturnValue = 40305; public const int LID_LegitimationPayloadStruct = 40400; public const int LID_LegitimationPayloadType = 40401; public const int LID_LegitimationPayloadUsername = 40402; public const int LID_LegitimationPayloadPassword = 40403; + public const uint ReleaseMngmtRoot_Rid = 2303328256; + + public const uint Constants = 0x8a110000; + public const int TI_BOOL = 0x02000000 + 1; public const int TI_BYTE = 0x02000000 + 2; public const int TI_CHAR = 0x02000000 + 3; @@ -138,4 +317,20 @@ public static class Ids public const int TI_WSTRING_START = 0x020b0000; // Start for WString[0] public const int TI_WSTRING_END = 0x020bffff; // End (WString[65535]) } + + public enum AlarmMessageType + { + Invalid = 0, + Alarm = 1, + Notify = 2, + InfoReport = 3, + EventAck = 4 + } + + public enum AlarmEnableDisableResult + { + Ok = 0, + InvalidRid = 32769, + InvalidAlid = 32770 + } } diff --git a/src/S7CommPlusDriver/Core/InitSslRequest.cs b/src/S7CommPlusDriver/Core/InitSslRequest.cs index 49f92e8..f7f3f1c 100644 --- a/src/S7CommPlusDriver/Core/InitSslRequest.cs +++ b/src/S7CommPlusDriver/Core/InitSslRequest.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * diff --git a/src/S7CommPlusDriver/Core/InitSslResponse.cs b/src/S7CommPlusDriver/Core/InitSslResponse.cs index b20eaf1..c5f4bcb 100644 --- a/src/S7CommPlusDriver/Core/InitSslResponse.cs +++ b/src/S7CommPlusDriver/Core/InitSslResponse.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * diff --git a/src/S7CommPlusDriver/Core/Notification.cs b/src/S7CommPlusDriver/Core/Notification.cs index 21a5e19..d6dd6d6 100644 --- a/src/S7CommPlusDriver/Core/Notification.cs +++ b/src/S7CommPlusDriver/Core/Notification.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -19,7 +19,7 @@ namespace S7CommPlusDriver { - public class Notification + internal class Notification { public byte ProtocolVersion { get; set; } @@ -37,17 +37,20 @@ public class Notification public Dictionary Values; // Item reference number, Value public Dictionary ReturnValues; // Item reference number, ReturnValue + public Dictionary OnlineStatusTableValues; // Item reference number, raw status-table word public UInt32 P2SubscriptionObjectId; // for alarm object public UInt16 P2Unknown1; // for alarm object public byte P2ReturnValue; // for alarm object public List P2Objects; // for alarm object + public byte[] TrailingBytes; // for non-alarm notification extensions public Notification(byte protocolVersion) { ProtocolVersion = protocolVersion; Values = new Dictionary(); ReturnValues = new Dictionary(); + OnlineStatusTableValues = new Dictionary(); } public int Deserialize(Stream buffer) @@ -56,7 +59,6 @@ public int Deserialize(Stream buffer) byte subscrccnt; byte item_return_value; UInt32 itemref; - UInt32 dummy; byte PeekByte; ret += S7p.DecodeUInt32(buffer, out SubscriptionObjectId); @@ -85,8 +87,7 @@ public int Deserialize(Stream buffer) } // Return value: If the value != 0 then follows a dataset with the common known structure. // If an access error occurs, we have here an error-value, in this case datatype==NULL. - // TODO: The returncodes follow not any known structure. I've tried to reproduce some errors - // on different controllers and generations with the following results: + // Known observed return codes from different controllers and generations: // hex bin ref-id value description // 0x03 = 0000 0011 -> ntohl - Addressing error (S7-1500 - Plcsim), like 0x13 // 0x13 = 0001 0011 -> ntohl - Addressing error (S7-1200) and 1500-Plcsim @@ -98,30 +99,34 @@ public int Deserialize(Stream buffer) do { ret += S7p.DecodeByte(buffer, out item_return_value); - switch (item_return_value) + var returnCode = (S7CommPlusNotificationReturnCode)item_return_value; + switch (returnCode) { - case 0x00: + case S7CommPlusNotificationReturnCode.EndOfList: break; - case 0x92: + case S7CommPlusNotificationReturnCode.ValueWithUInt32Reference: // Item reference number: Is sent to plc in the subscription-telegram for the addresses. ret += S7p.DecodeUInt32(buffer, out itemref); Values.Add(itemref, PValue.Deserialize(buffer)); break; - case 0x9b: + case S7CommPlusNotificationReturnCode.ValueWithVlqReference: ret += S7p.DecodeUInt32Vlq(buffer, out itemref); Values.Add(itemref, PValue.Deserialize(buffer)); break; - case 0x9c: - // Don't do anything with the data (for now) - ret += S7p.DecodeUInt32(buffer, out dummy); + case S7CommPlusNotificationReturnCode.OnlineStatusTable: + // Observed online status-table item shape: + // 9C rr ss ss vv, where rr is the subscription item reference, + // ss ss is a status-table field id, and vv is the compact status value. + ret += S7p.DecodeUInt32(buffer, out var onlineStatusTableValue); + OnlineStatusTableValues[onlineStatusTableValue >> 24] = onlineStatusTableValue; break; - case 0x13: - case 0x03: + case S7CommPlusNotificationReturnCode.AddressingError1200: + case S7CommPlusNotificationReturnCode.AddressingError: ret += S7p.DecodeUInt32(buffer, out itemref); ReturnValues.Add(itemref, item_return_value); break; //case 0x81: //Only in protocol version v1, but also used in S7-1500 in part 2 for ProgramAlarm - case 0x83: + case S7CommPlusNotificationReturnCode.LegacyValue: // Probably only in protocol version v1 throw new NotImplementedException(); default: @@ -136,20 +141,37 @@ public int Deserialize(Stream buffer) buffer.Position -= 1; if (PeekByte != 0) { + var part2Start = buffer.Position; ret += S7p.DecodeUInt32(buffer, out P2SubscriptionObjectId); ret += S7p.DecodeUInt16(buffer, out P2Unknown1); ret += S7p.DecodeByte(buffer, out P2ReturnValue); // It's not known if there are more than one object (as List), each object has // it's return value, or if there is really only one. // I wasn't able to produce a notification with more than one. - if (P2ReturnValue == 0x81) + if ((S7CommPlusNotificationReturnCode)P2ReturnValue == S7CommPlusNotificationReturnCode.AlarmObject) { P2Objects = new List(); ret += S7p.DecodeObjectList(buffer, ref P2Objects); } else { - throw new NotImplementedException(); + buffer.Position = part2Start; + var remaining = (int)(buffer.Length - buffer.Position); + TrailingBytes = new byte[remaining]; + if (remaining > 0) + { + var readTotal = 0; + while (readTotal < remaining) + { + var read = buffer.Read(TrailingBytes, readTotal, remaining - readTotal); + if (read == 0) + { + throw new EndOfStreamException(); + } + readTotal += read; + } + ret += remaining; + } } } return ret; @@ -185,19 +207,37 @@ public override string ToString() s += "" + Environment.NewLine; s += "" + errval.Key.ToString() + "" + Environment.NewLine; s += "" + errval.Value.ToString() + "" + Environment.NewLine; + s += "" + ((S7CommPlusNotificationReturnCode)errval.Value).ToString() + "" + Environment.NewLine; s += "" + Environment.NewLine; } s += "" + Environment.NewLine; + s += "" + Environment.NewLine; + foreach (var value in OnlineStatusTableValues) + { + s += "" + Environment.NewLine; + s += "" + value.Key.ToString() + "" + Environment.NewLine; + s += "" + value.Value.ToString() + "" + Environment.NewLine; + s += "" + Environment.NewLine; + } + s += "" + Environment.NewLine; // For alarm object(s) s += "" + P2SubscriptionObjectId.ToString() + "" + Environment.NewLine; s += "" + P2Unknown1.ToString() + "" + Environment.NewLine; s += "" + P2ReturnValue.ToString() + "" + Environment.NewLine; + s += "" + ((S7CommPlusNotificationReturnCode)P2ReturnValue).ToString() + "" + Environment.NewLine; s += "" + Environment.NewLine; - foreach (var p2o in P2Objects) + if (P2Objects != null) { - s += p2o.ToString(); + foreach (var p2o in P2Objects) + { + s += p2o.ToString(); + } } s += "" + Environment.NewLine; + if (TrailingBytes != null) + { + s += "" + BitConverter.ToString(TrailingBytes) + "" + Environment.NewLine; + } s += "" + Environment.NewLine; return s; } diff --git a/src/S7CommPlusDriver/Core/PObject.cs b/src/S7CommPlusDriver/Core/PObject.cs index f16f3b8..6315822 100644 --- a/src/S7CommPlusDriver/Core/PObject.cs +++ b/src/S7CommPlusDriver/Core/PObject.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -19,7 +19,7 @@ namespace S7CommPlusDriver { - public class PObject : IS7pSerialize + internal class PObject : IS7pSerialize { public UInt32 RelationId; public UInt32 ClassId; @@ -70,9 +70,8 @@ public void SetVarnameList(PVarnameList namelist) public void AddObject(PObject obj) { - // Whether using the ClassId as Key makes sense, remains to be seen - // TODO: The ClassId is not unique and may be occur more than once - // (e.g. DB.Class_Rid and in RelId is the DB number as DB.1) + // ClassId is not unique by itself, so child objects are keyed by + // both ClassId and RelationId (for example DB.Class_Rid + DB.1). var tuple = new Tuple(obj.ClassId, obj.RelationId); Objects.Add(tuple, obj); } diff --git a/src/S7CommPlusDriver/Core/POffsetInfoType.cs b/src/S7CommPlusDriver/Core/POffsetInfoType.cs index 9c81d7a..1972a65 100644 --- a/src/S7CommPlusDriver/Core/POffsetInfoType.cs +++ b/src/S7CommPlusDriver/Core/POffsetInfoType.cs @@ -676,7 +676,7 @@ public override string ToString() } } - public class POffsetInfoType_FbArray : POffsetInfoType, IOffsetInfoType_Relation + public class POffsetInfoType_FbArray : POffsetInfoType, IOffsetInfoType_Relation, IOffsetInfoType_MDim { public UInt16 UnspecifiedOffsetinfo1; public UInt16 UnspecifiedOffsetinfo2; @@ -695,8 +695,8 @@ public class POffsetInfoType_FbArray : POffsetInfoType, IOffsetInfoType_Relation public UInt32[] MdimArrayElementCount = new UInt32[6]; public override bool HasRelation() { return true; } - public override bool Is1Dim() { return false; } //!!! TODO - public override bool IsMDim() { return false; } //!!! TODO + public override bool Is1Dim() { return false; } + public override bool IsMDim() { return true; } public static POffsetInfoType_FbArray Deserialize(Stream buffer, out int length) { @@ -775,5 +775,25 @@ public uint GetRelationId() { return RelationId; } + + public Int32 GetArrayLowerBounds() + { + return MdimArrayLowerBounds[0]; + } + + public UInt32 GetArrayElementCount() + { + return ArrayElementCount; + } + + public Int32[] GetMdimArrayLowerBounds() + { + return MdimArrayLowerBounds; + } + + public UInt32[] GetMdimArrayElementCount() + { + return MdimArrayElementCount; + } } } diff --git a/src/S7CommPlusDriver/Core/PValue.cs b/src/S7CommPlusDriver/Core/PValue.cs index 68a8625..e6482cb 100644 --- a/src/S7CommPlusDriver/Core/PValue.cs +++ b/src/S7CommPlusDriver/Core/PValue.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -19,8 +19,8 @@ namespace S7CommPlusDriver { - // TODO: Maybe there's room for improvement, as the array classes duplicate many code of the base classes - public abstract class PValue : IS7pSerialize + // Array variants intentionally keep their own serializers because PLC wire flags differ per collection shape. + internal abstract class PValue : IS7pSerialize { protected static byte FLAGS_ARRAY = 0x10; protected static byte FLAGS_ADDRESSARRAY = 0x20; @@ -195,8 +195,7 @@ public static PValue Deserialize(Stream buffer, bool disableVlq = false) return null; } } - - public class ValueNull : PValue + internal class ValueNull : PValue { public ValueNull() : this(0) { @@ -225,8 +224,7 @@ public static ValueNull Deserialize(Stream buffer, byte flags) return new ValueNull(flags); } } - - public class ValueBool : PValue + internal class ValueBool : PValue { bool Value; @@ -273,7 +271,7 @@ public static ValueBool Deserialize(Stream buffer, byte flags) /// An Array[0..9] will be transmitted as 16 elements and so on. /// At this time, serialize doesn't respect the padding elements, must be done on a higher level. /// - public class ValueBoolArray : PValue + internal class ValueBoolArray : PValue { bool[] Value; @@ -301,7 +299,7 @@ public override int Serialize(Stream buffer) int ret = 0; ret += S7p.EncodeByte(buffer, DatatypeFlags); ret += S7p.EncodeByte(buffer, Datatype.Bool); - // TODO: Should we respect the padding inside this class, or at a higher level? + // Bool-array padding is handled by the caller because packed/byte-wise layout depends on the PLC offset type. ret += S7p.EncodeUInt32Vlq(buffer, (uint)Value.Length); for (int i = 0; i < Value.Length; i++) { @@ -343,8 +341,7 @@ public static ValueBoolArray Deserialize(Stream buffer, byte flags, bool disable return new ValueBoolArray(value, flags); } } - - public class ValueUSInt : PValue + internal class ValueUSInt : PValue { byte Value; @@ -384,8 +381,7 @@ public static ValueUSInt Deserialize(Stream buffer, byte flags) return new ValueUSInt(value, flags); } } - - public class ValueUSIntArray : PValue + internal class ValueUSIntArray : PValue { byte[] Value; @@ -452,8 +448,7 @@ public static ValueUSIntArray Deserialize(Stream buffer, byte flags, bool disabl return new ValueUSIntArray(value, flags); } } - - public class ValueUInt : PValue + internal class ValueUInt : PValue { UInt16 Value; @@ -493,8 +488,7 @@ public static ValueUInt Deserialize(Stream buffer, byte flags) return new ValueUInt(value, flags); } } - - public class ValueUIntArray : PValue + internal class ValueUIntArray : PValue { UInt16[] Value; @@ -561,8 +555,7 @@ public static ValueUIntArray Deserialize(Stream buffer, byte flags, bool disable return new ValueUIntArray(value, flags); } } - - public class ValueUDInt : PValue + internal class ValueUDInt : PValue { UInt32 Value; @@ -609,8 +602,7 @@ public static ValueUDInt Deserialize(Stream buffer, byte flags, bool disableVlq) return new ValueUDInt(value, flags); } } - - public class ValueUDIntArray : PValue + internal class ValueUDIntArray : PValue { UInt32[] Value; @@ -687,7 +679,7 @@ public static ValueUDIntArray Deserialize(Stream buffer, byte flags, bool disabl // All elements are kind of key,value. And Value is of the selected type. // The list is terminated by Null. // E.g.: Reading 1037 (SystemLimits) via GetVarSubStreamed - public class ValueUDIntSparseArray : PValue + internal class ValueUDIntSparseArray : PValue { Dictionary Value; @@ -762,8 +754,7 @@ public static ValueUDIntSparseArray Deserialize(Stream buffer, byte flags, bool return new ValueUDIntSparseArray(value, flags); } } - - public class ValueULInt : PValue + internal class ValueULInt : PValue { UInt64 Value; @@ -810,8 +801,7 @@ public static ValueULInt Deserialize(Stream buffer, byte flags, bool disableVlq) return new ValueULInt(value, flags); } } - - public class ValueULIntArray : PValue + internal class ValueULIntArray : PValue { UInt64[] Value; @@ -883,8 +873,7 @@ public static ValueULIntArray Deserialize(Stream buffer, byte flags, bool disabl return new ValueULIntArray(value, flags); } } - - public class ValueSInt : PValue + internal class ValueSInt : PValue { sbyte Value; @@ -924,8 +913,7 @@ public static ValueSInt Deserialize(Stream buffer, byte flags) return new ValueSInt((sbyte)value, flags); } } - - public class ValueSIntArray : PValue + internal class ValueSIntArray : PValue { sbyte[] Value; @@ -994,8 +982,7 @@ public static ValueSIntArray Deserialize(Stream buffer, byte flags, bool disable return new ValueSIntArray(value, flags); } } - - public class ValueInt : PValue + internal class ValueInt : PValue { Int16 Value; @@ -1035,8 +1022,7 @@ public static ValueInt Deserialize(Stream buffer, byte flags) return new ValueInt(value, flags); } } - - public class ValueIntArray : PValue + internal class ValueIntArray : PValue { Int16[] Value; @@ -1103,8 +1089,7 @@ public static ValueIntArray Deserialize(Stream buffer, byte flags, bool disableV return new ValueIntArray(value, flags); } } - - public class ValueDInt : PValue + internal class ValueDInt : PValue { Int32 Value; @@ -1151,8 +1136,7 @@ public static ValueDInt Deserialize(Stream buffer, byte flags, bool disableVlq) return new ValueDInt(value, flags); } } - - public class ValueDIntArray : PValue + internal class ValueDIntArray : PValue { Int32[] Value; @@ -1224,8 +1208,7 @@ public static ValueDIntArray Deserialize(Stream buffer, byte flags, bool disable return new ValueDIntArray(value, flags); } } - - public class ValueDIntSparseArray : PValue + internal class ValueDIntSparseArray : PValue { Dictionary Value; @@ -1300,8 +1283,7 @@ public static ValueDIntSparseArray Deserialize(Stream buffer, byte flags, bool d return new ValueDIntSparseArray(value, flags); } } - - public class ValueLInt : PValue + internal class ValueLInt : PValue { Int64 Value; @@ -1348,8 +1330,7 @@ public static ValueLInt Deserialize(Stream buffer, byte flags, bool disableVlq) return new ValueLInt(value, flags); } } - - public class ValueLIntArray : PValue + internal class ValueLIntArray : PValue { Int64[] Value; @@ -1421,8 +1402,7 @@ public static ValueLIntArray Deserialize(Stream buffer, byte flags, bool disable return new ValueLIntArray(value, flags); } } - - public class ValueByte : PValue + internal class ValueByte : PValue { byte Value; @@ -1462,8 +1442,7 @@ public static ValueByte Deserialize(Stream buffer, byte flags) return new ValueByte(value, flags); } } - - public class ValueByteArray : PValue + internal class ValueByteArray : PValue { byte[] Value; @@ -1530,8 +1509,7 @@ public static ValueByteArray Deserialize(Stream buffer, byte flags, bool disable return new ValueByteArray(value, flags); } } - - public class ValueWord : PValue + internal class ValueWord : PValue { UInt16 Value; @@ -1571,8 +1549,7 @@ public static ValueWord Deserialize(Stream buffer, byte flags) return new ValueWord(value, flags); } } - - public class ValueWordArray : PValue + internal class ValueWordArray : PValue { UInt16[] Value; @@ -1639,8 +1616,7 @@ public static ValueWordArray Deserialize(Stream buffer, byte flags, bool disable return new ValueWordArray(value, flags); } } - - public class ValueDWord : PValue + internal class ValueDWord : PValue { UInt32 Value; @@ -1680,8 +1656,7 @@ public static ValueDWord Deserialize(Stream buffer, byte flags) return new ValueDWord(value, flags); } } - - public class ValueDWordArray : PValue + internal class ValueDWordArray : PValue { UInt32[] Value; @@ -1748,8 +1723,7 @@ public static ValueDWordArray Deserialize(Stream buffer, byte flags, bool disabl return new ValueDWordArray(value, flags); } } - - public class ValueLWord : PValue + internal class ValueLWord : PValue { UInt64 Value; @@ -1789,8 +1763,7 @@ public static ValueLWord Deserialize(Stream buffer, byte flags) return new ValueLWord(value, flags); } } - - public class ValueLWordArray : PValue + internal class ValueLWordArray : PValue { UInt64[] Value; @@ -1857,8 +1830,7 @@ public static ValueLWordArray Deserialize(Stream buffer, byte flags, bool disabl return new ValueLWordArray(value, flags); } } - - public class ValueReal : PValue + internal class ValueReal : PValue { float Value; @@ -1898,8 +1870,7 @@ public static ValueReal Deserialize(Stream buffer, byte flags) return new ValueReal(value, flags); } } - - public class ValueRealArray : PValue + internal class ValueRealArray : PValue { float[] Value; @@ -1966,8 +1937,7 @@ public static ValueRealArray Deserialize(Stream buffer, byte flags, bool disable return new ValueRealArray(value, flags); } } - - public class ValueLReal : PValue + internal class ValueLReal : PValue { double Value; @@ -2007,8 +1977,7 @@ public static ValueLReal Deserialize(Stream buffer, byte flags) return new ValueLReal(value, flags); } } - - public class ValueLRealArray : PValue + internal class ValueLRealArray : PValue { double[] Value; @@ -2075,8 +2044,7 @@ public static ValueLRealArray Deserialize(Stream buffer, byte flags, bool disabl return new ValueLRealArray(value, flags); } } - - public class ValueTimestamp : PValue + internal class ValueTimestamp : PValue { UInt64 Value; @@ -2149,8 +2117,7 @@ public static ValueTimestamp Deserialize(Stream buffer, byte flags) return new ValueTimestamp(value, flags); } } - - public class ValueTimestampArray : PValue + internal class ValueTimestampArray : PValue { UInt64[] Value; @@ -2213,8 +2180,7 @@ public static ValueTimestampArray Deserialize(Stream buffer, byte flags, bool di return new ValueTimestampArray(value, flags); } } - - public class ValueTimespan : PValue + internal class ValueTimespan : PValue { Int64 Value; @@ -2307,8 +2273,7 @@ public static ValueTimespan Deserialize(Stream buffer, byte flags, bool disableV return new ValueTimespan(value, flags); } } - - public class ValueTimespanArray : PValue + internal class ValueTimespanArray : PValue { Int64[] Value; @@ -2382,8 +2347,7 @@ public static ValueTimespanArray Deserialize(Stream buffer, byte flags, bool dis return new ValueTimespanArray(value, flags); } } - - public class ValueRID : PValue + internal class ValueRID : PValue { UInt32 Value; @@ -2423,8 +2387,7 @@ public static ValueRID Deserialize(Stream buffer, byte flags) return new ValueRID(value, flags); } } - - public class ValueRIDArray : PValue + internal class ValueRIDArray : PValue { UInt32[] Value; @@ -2491,8 +2454,7 @@ public static ValueRIDArray Deserialize(Stream buffer, byte flags, bool disableV return new ValueRIDArray(value, flags); } } - - public class ValueAID : PValue + internal class ValueAID : PValue { UInt32 Value; @@ -2539,8 +2501,7 @@ public static ValueAID Deserialize(Stream buffer, byte flags, bool disableVlq) return new ValueAID(value, flags); } } - - public class ValueAIDArray : PValue + internal class ValueAIDArray : PValue { UInt32[] Value; @@ -2612,8 +2573,7 @@ public static ValueAIDArray Deserialize(Stream buffer, byte flags, bool disableV return new ValueAIDArray(value, flags); } } - - public class ValueBlob : PValue + internal class ValueBlob : PValue { public UInt32 BlobRootId; byte[] Value; @@ -2728,8 +2688,7 @@ public static ValueBlob Deserialize(Stream buffer, byte flags, bool disableVlq) return blob; } } - - public class ValueBlobArray : PValue + internal class ValueBlobArray : PValue { ValueBlob[] Value; @@ -2795,8 +2754,7 @@ public static ValueBlobArray Deserialize(Stream buffer, byte flags, bool disable return new ValueBlobArray(value, flags); } } - - public class ValueBlobSparseArray : PValue + internal class ValueBlobSparseArray : PValue { public struct BlobEntry { @@ -2893,8 +2851,7 @@ public static ValueBlobSparseArray Deserialize(Stream buffer, byte flags, bool d return new ValueBlobSparseArray(value, flags); } } - - public class ValueWString : PValue + internal class ValueWString : PValue { string Value; @@ -2944,8 +2901,7 @@ public static ValueWString Deserialize(Stream buffer, byte flags, bool disableVl return new ValueWString(value, flags); } } - - public class ValueWStringArray : PValue + internal class ValueWStringArray : PValue { string[] Value; @@ -3017,8 +2973,7 @@ public static ValueWStringArray Deserialize(Stream buffer, byte flags, bool disa return new ValueWStringArray(value, flags); } } - - public class ValueWStringSparseArray : PValue + internal class ValueWStringSparseArray : PValue { Dictionary Value; @@ -3097,8 +3052,7 @@ public static ValueWStringSparseArray Deserialize(Stream buffer, byte flags, boo return new ValueWStringSparseArray(value, flags); } } - - public class ValueStruct : PValue + internal class ValueStruct : PValue { UInt32 Value; private Dictionary Elements = new Dictionary(); @@ -3151,7 +3105,6 @@ public override int Serialize(Stream buffer) ret += S7p.EncodeByte(buffer, DatatypeFlags); ret += S7p.EncodeByte(buffer, Datatype.Struct); ret += S7p.EncodeUInt32(buffer, Value); - // TODO: EXPERIMENTAL! // Packed Struct, see comment in Deserialize if ((Value > 0x90000000 && Value < 0x9fffffff) || (Value > 0x02000000 && Value < 0x02ffffff)) { @@ -3178,7 +3131,7 @@ public override int Serialize(Stream buffer) } else { - Console.WriteLine("ValueStruct.Serialize(): Elements[0] is not of type ValueByteArray"); + System.Diagnostics.Trace.WriteLine("ValueStruct.Serialize(): Elements[0] is not of type ValueByteArray"); } } } diff --git a/src/S7CommPlusDriver/Core/PVarnameList.cs b/src/S7CommPlusDriver/Core/PVarnameList.cs index 44c7c2d..fc03343 100644 --- a/src/S7CommPlusDriver/Core/PVarnameList.cs +++ b/src/S7CommPlusDriver/Core/PVarnameList.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -19,7 +19,7 @@ namespace S7CommPlusDriver { - public class PVarnameList + internal class PVarnameList { public List Names; @@ -38,8 +38,14 @@ public int Deserialize(Stream buffer) maxret = ret + blocklen; while (blocklen > 0) { - do + if (maxret > buffer.Length) { + throw new InvalidDataException("Variable-name block length exceeds the available payload."); + } + + while (ret < maxret) + { + var positionBefore = buffer.Position; name = String.Empty; // Length of a name is max. 128 chars ret += S7p.DecodeByte(buffer, out namelen); @@ -47,7 +53,11 @@ public int Deserialize(Stream buffer) Names.Add(name); // Additional 1 Byte with 0 at the end. Why Null termination when the length is given? I don't know... ret += S7p.DecodeByte(buffer, out unknown2); - } while (ret < maxret); + if (buffer.Position <= positionBefore) + { + throw new InvalidDataException("Variable-name parser made no progress."); + } + } ret += S7p.DecodeUInt16(buffer, out blocklen); maxret = ret + blocklen; } diff --git a/src/S7CommPlusDriver/Core/PVartypeList.cs b/src/S7CommPlusDriver/Core/PVartypeList.cs index b4aa669..ce2f0a7 100644 --- a/src/S7CommPlusDriver/Core/PVartypeList.cs +++ b/src/S7CommPlusDriver/Core/PVartypeList.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -19,7 +19,7 @@ namespace S7CommPlusDriver { - public class PVartypeList + internal class PVartypeList { public List Elements; public UInt32 FirstId; @@ -39,12 +39,22 @@ public int Deserialize(Stream buffer) while (blocklen > 0) { - do + if (maxret > buffer.Length) { + throw new InvalidDataException("Variable-type block length exceeds the available payload."); + } + + while (ret < maxret) + { + var positionBefore = buffer.Position; var elem = new PVartypeListElement(); ret += elem.Deserialize(buffer); Elements.Add(elem); - } while (ret < maxret); + if (buffer.Position <= positionBefore) + { + throw new InvalidDataException("Variable-type parser made no progress."); + } + } ret += S7p.DecodeUInt16(buffer, out blocklen); maxret = ret + blocklen; } @@ -69,7 +79,7 @@ public override string ToString() } } - public class PVartypeListElement + internal class PVartypeListElement { /* flags in tag description for 1500 */ const int S7COMMP_TAGDESCR_ATTRIBUTE2_OFFSETINFOTYPE = 0xf000; /* Bits 13..16 */ diff --git a/src/S7CommPlusDriver/Core/CommRessources.cs b/src/S7CommPlusDriver/Core/S7CommPlusCommunicationResourceSnapshot.cs similarity index 78% rename from src/S7CommPlusDriver/Core/CommRessources.cs rename to src/S7CommPlusDriver/Core/S7CommPlusCommunicationResourceSnapshot.cs index b2e7651..4c65dde 100644 --- a/src/S7CommPlusDriver/Core/CommRessources.cs +++ b/src/S7CommPlusDriver/Core/S7CommPlusCommunicationResourceSnapshot.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -15,10 +15,11 @@ using System; using System.Collections.Generic; +using S7CommPlusDriver.Internal; namespace S7CommPlusDriver { - public class CommRessources + internal sealed class S7CommPlusCommunicationResourceSnapshot { public int TagsPerReadRequestMax { get; private set; } = 20; public int TagsPerWriteRequestMax { get; private set; } = 20; @@ -29,12 +30,13 @@ public class CommRessources public int SubscriptionMemoryMax { get; private set; } public int SubscriptionMemoryFree { get; private set; } - public int ReadMax(S7CommPlusConnection conn) + public int ReadMax(S7CommPlusProtocolSession conn) { // Read SystemLimits // Assumption (so far, because for all CPUs which have be seen both values were the same): - // 1000 = Number for Reading - // 1001 = Number for Writing + // Siemens SystemLimits LIDs: + // 1000 = number of tags per read request + // 1001 = number of tags per write request int res; var readlist = new List(); var values = new List(); @@ -45,35 +47,35 @@ public int ReadMax(S7CommPlusConnection conn) AccessArea = Ids.ObjectRoot, AccessSubArea = Ids.SystemLimits }; - adrTagsPerReadRequestMax.LID.Add(1000); + adrTagsPerReadRequestMax.LID.Add(S7CommPlusProtocolConstants.SystemLimitTagsPerReadRequest); var adrTagsPerWriteRequestMax = new ItemAddress { AccessArea = Ids.ObjectRoot, AccessSubArea = Ids.SystemLimits }; - adrTagsPerWriteRequestMax.LID.Add(1001); + adrTagsPerWriteRequestMax.LID.Add(S7CommPlusProtocolConstants.SystemLimitTagsPerWriteRequest); var adrPlcSubscriptionsMax = new ItemAddress { AccessArea = Ids.ObjectRoot, AccessSubArea = Ids.SystemLimits }; - adrPlcSubscriptionsMax.LID.Add(0); + adrPlcSubscriptionsMax.LID.Add(S7CommPlusProtocolConstants.SystemLimitPlcSubscriptions); var adrPlcAttributesMax = new ItemAddress { AccessArea = Ids.ObjectRoot, AccessSubArea = Ids.SystemLimits }; - adrPlcAttributesMax.LID.Add(1); + adrPlcAttributesMax.LID.Add(S7CommPlusProtocolConstants.SystemLimitPlcAttributes); var adrSubscriptionMemoryMax = new ItemAddress { AccessArea = Ids.ObjectRoot, AccessSubArea = Ids.SystemLimits }; - adrSubscriptionMemoryMax.LID.Add(2); + adrSubscriptionMemoryMax.LID.Add(S7CommPlusProtocolConstants.SystemLimitSubscriptionMemory); readlist.Add(adrTagsPerReadRequestMax); readlist.Add(adrTagsPerWriteRequestMax); @@ -85,9 +87,9 @@ public int ReadMax(S7CommPlusConnection conn) int i = 0; for (i = 0; i < values.Count; i++) { - if (values[i] != null && errors[i] == 0) + if (i < errors.Count && errors[i] == 0 && values[i] is ValueDInt value) { - int v = ((ValueDInt)values[i]).GetValue(); + int v = value.GetValue(); switch (i) { case 0: @@ -111,7 +113,7 @@ public int ReadMax(S7CommPlusConnection conn) return res; } - public int ReadFree(S7CommPlusConnection conn) + public int ReadFree(S7CommPlusProtocolSession conn) { int res; var readlist = new List(); @@ -123,21 +125,21 @@ public int ReadFree(S7CommPlusConnection conn) AccessArea = Ids.ObjectRoot, AccessSubArea = Ids.FreeItems }; - adrPlcSubscriptionsFree.LID.Add(0); + adrPlcSubscriptionsFree.LID.Add(S7CommPlusProtocolConstants.SystemLimitPlcSubscriptions); var adrPlcAttributesFree = new ItemAddress { AccessArea = Ids.ObjectRoot, AccessSubArea = Ids.FreeItems }; - adrPlcAttributesFree.LID.Add(1); + adrPlcAttributesFree.LID.Add(S7CommPlusProtocolConstants.SystemLimitPlcAttributes); var adrSubscriptionMemoryFree = new ItemAddress { AccessArea = Ids.ObjectRoot, AccessSubArea = Ids.FreeItems }; - adrSubscriptionMemoryFree.LID.Add(2); + adrSubscriptionMemoryFree.LID.Add(S7CommPlusProtocolConstants.SystemLimitSubscriptionMemory); readlist.Add(adrPlcSubscriptionsFree); readlist.Add(adrPlcAttributesFree); @@ -147,9 +149,9 @@ public int ReadFree(S7CommPlusConnection conn) int i = 0; for (i = 0; i < values.Count; i++) { - if (values[i] != null && errors[i] == 0) + if (i < errors.Count && errors[i] == 0 && values[i] is ValueDInt value) { - int v = ((ValueDInt)values[i]).GetValue(); + int v = value.GetValue(); switch (i) { case 0: @@ -169,7 +171,7 @@ public int ReadFree(S7CommPlusConnection conn) public override string ToString() { - string s = "" + Environment.NewLine; + string s = "" + Environment.NewLine; s += "" + TagsPerReadRequestMax.ToString() + "" + Environment.NewLine; s += "" + TagsPerWriteRequestMax.ToString() + "" + Environment.NewLine; s += "" + PlcAttributesMax.ToString() + "" + Environment.NewLine; @@ -178,8 +180,8 @@ public override string ToString() s += "" + PlcSubscriptionsFree.ToString() + "" + Environment.NewLine; s += "" + SubscriptionMemoryMax.ToString() + "" + Environment.NewLine; s += "" + SubscriptionMemoryFree.ToString() + "" + Environment.NewLine; - s += "" + Environment.NewLine; + s += "" + Environment.NewLine; return s; } } -} \ No newline at end of file +} diff --git a/src/S7CommPlusDriver/Core/S7p.cs b/src/S7CommPlusDriver/Core/S7p.cs index e466424..3b47f4b 100644 --- a/src/S7CommPlusDriver/Core/S7p.cs +++ b/src/S7CommPlusDriver/Core/S7p.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -16,11 +16,27 @@ using System; using System.Text; using System.Collections.Generic; +using S7CommPlusDriver.Internal; namespace S7CommPlusDriver { - public class S7p + internal class S7p { + private static bool TryReadExactly(System.IO.Stream buffer, byte[] value, int length) + { + var totalRead = 0; + while (totalRead < length) + { + var bytesRead = buffer.Read(value, totalRead, length - totalRead); + if (bytesRead == 0) + { + return false; + } + totalRead += bytesRead; + } + return true; + } + public static int EncodeByte(System.IO.Stream buffer, byte value) { buffer.WriteByte((byte)value); @@ -184,7 +200,11 @@ public static int DecodeUInt64(System.IO.Stream buffer, out UInt64 value) return 0; } byte[] b = new byte[8]; - buffer.Read(b, 0, 8); + if (!TryReadExactly(buffer, b, 8)) + { + value = 0; + return 0; + } Array.Reverse(b, 0, 8); value = BitConverter.ToUInt64(b, 0); return 8; @@ -198,7 +218,11 @@ public static int DecodeInt64(System.IO.Stream buffer, out Int64 value) return 0; } byte[] b = new byte[8]; - buffer.Read(b, 0, 8); + if (!TryReadExactly(buffer, b, 8)) + { + value = 0; + return 0; + } Array.Reverse(b, 0, 8); value = BitConverter.ToInt64(b, 0); return 8; @@ -233,7 +257,12 @@ public static int DecodeUInt32Vlq(System.IO.Stream buffer, out UInt32 value) int length = 0; for (counter = 1; counter <= 5; counter++) { - octet = (byte)buffer.ReadByte(); + int read = buffer.ReadByte(); + if (read < 0) + { + throw new System.IO.InvalidDataException("Truncated UInt32 VLQ value."); + } + octet = (byte)read; length++; val <<= 7; cont = (byte)(octet & 0x80); @@ -257,7 +286,12 @@ public static int DecodeInt32Vlq(System.IO.Stream buffer, out Int32 value) int length = 0; for (counter = 1; counter <= 5; counter++) { - octet = (byte)buffer.ReadByte(); + int read = buffer.ReadByte(); + if (read < 0) + { + throw new System.IO.InvalidDataException("Truncated Int32 VLQ value."); + } + octet = (byte)read; length++; if ((counter == 1) && ((octet & 0x40) != 0)) { // check sign @@ -408,7 +442,12 @@ public static int DecodeUInt64Vlq(System.IO.Stream buffer, out UInt64 value) int length = 0; for (counter = 1; counter <= 8; counter++) { - octet = (byte)buffer.ReadByte(); + int read = buffer.ReadByte(); + if (read < 0) + { + throw new System.IO.InvalidDataException("Truncated UInt64 VLQ value."); + } + octet = (byte)read; length++; val <<= 7; cont = (byte)(octet & 0x80); @@ -421,7 +460,12 @@ public static int DecodeUInt64Vlq(System.IO.Stream buffer, out UInt64 value) } if (cont > 0) /* 8*7 bit + 8 bit = 64 bit -> Special case in last octet! */ { - octet = (byte)buffer.ReadByte(); + int read = buffer.ReadByte(); + if (read < 0) + { + throw new System.IO.InvalidDataException("Truncated UInt64 VLQ extension byte."); + } + octet = (byte)read; length++; val <<= 8; val += octet; @@ -439,7 +483,12 @@ public static int DecodeInt64Vlq(System.IO.Stream buffer, out Int64 value) int length = 0; for (counter = 1; counter <= 8; counter++) { - octet = (byte)buffer.ReadByte(); + int read = buffer.ReadByte(); + if (read < 0) + { + throw new System.IO.InvalidDataException("Truncated Int64 VLQ value."); + } + octet = (byte)read; length++; if ((counter == 1) && ((octet & 0x40) != 0)) { // check sign @@ -461,7 +510,12 @@ public static int DecodeInt64Vlq(System.IO.Stream buffer, out Int64 value) if (cont > 0) { // 8*7 bit + 8 bit = 64 bit -> Special case in last octet! - octet = (byte)buffer.ReadByte(); + int read = buffer.ReadByte(); + if (read < 0) + { + throw new System.IO.InvalidDataException("Truncated Int64 VLQ extension byte."); + } + octet = (byte)read; length++; val <<= 8; val += octet; @@ -606,7 +660,11 @@ public static int DecodeWString(System.IO.Stream buffer, int length, out string return 0; } byte[] tmp = new byte[length]; - buffer.Read(tmp, 0, length); + if (!TryReadExactly(buffer, tmp, length)) + { + value = string.Empty; + return 0; + } value = Encoding.UTF8.GetString(tmp); return tmp.Length; } @@ -626,7 +684,11 @@ public static int DecodeOctets(System.IO.Stream buffer, int length, out byte[] v return 0; } value = new byte[length]; - buffer.Read(value, 0, length); + if (!TryReadExactly(buffer, value, length)) + { + value = null; + return 0; + } return value.Length; } @@ -640,14 +702,25 @@ public static int DecodeObjectList(System.IO.Stream buffer, ref List ob byte tagId; objList = new List(); // Peek one byte and set buffer back - S7p.DecodeByte(buffer, out tagId); + if (S7p.DecodeByte(buffer, out tagId) == 0) + { + return ret; + } buffer.Position -= 1; while (tagId == ElementID.StartOfObject) { + var positionBefore = buffer.Position; PObject obj = null; ret += S7p.DecodeObject(buffer, ref obj, AsList: true); + if (buffer.Position <= positionBefore) + { + throw new System.IO.InvalidDataException("Object-list parser made no progress."); + } objList.Add(obj); - S7p.DecodeByte(buffer, out tagId); + if (S7p.DecodeByte(buffer, out tagId) == 0) + { + break; + } buffer.Position -= 1; } return ret; @@ -661,7 +734,12 @@ public static int DecodeObject(System.IO.Stream buffer, ref PObject obj, bool As int ret = 0; do { + var positionBefore = buffer.Position; ret += DecodeByte(buffer, out tagId); + if (buffer.Position <= positionBefore) + { + throw new System.IO.InvalidDataException("Object parser made no progress."); + } switch (tagId) { case ElementID.StartOfObject: @@ -672,8 +750,7 @@ public static int DecodeObject(System.IO.Stream buffer, ref PObject obj, bool As ret += DecodeUInt32Vlq(buffer, out obj.ClassId); ret += DecodeUInt32Vlq(buffer, out obj.ClassFlags); ret += DecodeUInt32Vlq(buffer, out obj.AttributeId); - // If a List is expected, don't add the objects coming next to the parent object - // TODO: May be it's better to always expect a list? Adding the following objects to the first as children must always be wrong. + // If a list is expected, keep following objects as siblings instead of adding them to the parent. if (AsList == false) { ret += DecodeObject(buffer, ref obj); @@ -735,7 +812,7 @@ public static int DecodeHeader(System.IO.Stream buffer, out byte version, out UI public static int EncodeHeader(System.IO.Stream buffer, byte version, UInt16 length) { - buffer.WriteByte(0x72); + buffer.WriteByte(S7CommPlusProtocolConstants.FrameMarker); buffer.WriteByte(version); EncodeUInt16(buffer, length); return 4; diff --git a/src/S7CommPlusDriver/Core/SetMultiVariablesRequest.cs b/src/S7CommPlusDriver/Core/SetMultiVariablesRequest.cs index c7b4300..f3db933 100644 --- a/src/S7CommPlusDriver/Core/SetMultiVariablesRequest.cs +++ b/src/S7CommPlusDriver/Core/SetMultiVariablesRequest.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -16,12 +16,13 @@ using System; using System.Collections.Generic; using System.IO; +using S7CommPlusDriver.Internal; namespace S7CommPlusDriver { - public class SetMultiVariablesRequest : IS7pRequest + internal class SetMultiVariablesRequest : IS7pRequest { - byte TransportFlags = 0x34; + byte TransportFlags = S7CommPlusProtocolConstants.RequestWithResponseTransportFlags; public UInt32 InObjectId; /* Special @@ -39,11 +40,12 @@ public class SetMultiVariablesRequest : IS7pRequest * But counting them is identically. * * The only misleading thing is, we have two addresslists as members for both use-cases. - * TODO + * AddressList is used for object-local writes; AddressListVar is used for symbolic variable writes. */ public List AddressList = new List(); public List AddressListVar = new List(); public List ValueList = new List(); + public bool UseRawAddressList; public uint SessionId { get; set; } public byte ProtocolVersion { get; set; } @@ -78,7 +80,7 @@ public int Serialize(Stream buffer) // Request set ret += S7p.EncodeUInt32(buffer, InObjectId); ret += S7p.EncodeUInt32Vlq(buffer, (UInt32)ValueList.Count); - if (InObjectId > 0) + if (InObjectId > 0 || UseRawAddressList) { ret += S7p.EncodeUInt32Vlq(buffer, (UInt32)AddressList.Count); foreach (UInt32 id in AddressList) @@ -148,7 +150,7 @@ public override string ToString() s += "" + Environment.NewLine; s += "" + InObjectId.ToString() + "" + Environment.NewLine; s += "" + ValueList.Count + "" + Environment.NewLine; - if (InObjectId > 0) + if (InObjectId > 0 || UseRawAddressList) { s += "" + AddressList.Count + "" + Environment.NewLine; s += "" + Environment.NewLine; diff --git a/src/S7CommPlusDriver/Core/SetMultiVariablesResponse.cs b/src/S7CommPlusDriver/Core/SetMultiVariablesResponse.cs index 4c6e190..0be9ff1 100644 --- a/src/S7CommPlusDriver/Core/SetMultiVariablesResponse.cs +++ b/src/S7CommPlusDriver/Core/SetMultiVariablesResponse.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -19,7 +19,7 @@ namespace S7CommPlusDriver { - public class SetMultiVariablesResponse : IS7pResponse + internal class SetMultiVariablesResponse : IS7pResponse { public byte TransportFlags; public UInt64 ReturnValue; @@ -56,7 +56,7 @@ public int Deserialize(Stream buffer) { ret += S7p.DecodeUInt64Vlq(buffer, out retval); ErrorValues.Add(itemnr, retval); - ret += S7p.DecodeUInt32Vlq(buffer, out itemnr); /// TODO: Is this correct? + ret += S7p.DecodeUInt32Vlq(buffer, out itemnr); } ret += S7p.DecodeUInt32Vlq(buffer, out uint iid); IntegrityId = iid; diff --git a/src/S7CommPlusDriver/Core/SetVariableRequest.cs b/src/S7CommPlusDriver/Core/SetVariableRequest.cs index b00bab1..f770bf9 100644 --- a/src/S7CommPlusDriver/Core/SetVariableRequest.cs +++ b/src/S7CommPlusDriver/Core/SetVariableRequest.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -15,12 +15,13 @@ using System; using System.IO; +using S7CommPlusDriver.Internal; namespace S7CommPlusDriver { - public class SetVariableRequest : IS7pRequest + internal class SetVariableRequest : IS7pRequest { - public byte TransportFlags = 0x34; + public byte TransportFlags = S7CommPlusProtocolConstants.RequestWithResponseTransportFlags; public UInt32 InObjectId; diff --git a/src/S7CommPlusDriver/Core/SetVariableResponse.cs b/src/S7CommPlusDriver/Core/SetVariableResponse.cs index 1baa178..ca2dadb 100644 --- a/src/S7CommPlusDriver/Core/SetVariableResponse.cs +++ b/src/S7CommPlusDriver/Core/SetVariableResponse.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -18,7 +18,7 @@ namespace S7CommPlusDriver { - public class SetVariableResponse : IS7pResponse + internal class SetVariableResponse : IS7pResponse { public byte TransportFlags; public UInt64 ReturnValue; diff --git a/src/S7CommPlusDriver/Core/SiemensOmsEnums.cs b/src/S7CommPlusDriver/Core/SiemensOmsEnums.cs new file mode 100644 index 0000000..cc86d5d --- /dev/null +++ b/src/S7CommPlusDriver/Core/SiemensOmsEnums.cs @@ -0,0 +1,59 @@ +namespace S7CommPlusDriver +{ + public enum SiemensOmsAsRole + { + NoRole = 0, + Hmi = 1, + TisModifying = 2, + TisMonitoring = 3, + EngineeringSystem = 4 + } + + public enum SiemensOmsProtocolType + { + NoProtocol = 0, + ClassicSps7 = 1, + S7PlusTransport = 2 + } + + public enum SiemensOmsProtectionLevel + { + NotConfigured = 0, + NoProtection = 1, + WriteProtection = 2, + ReadWriteProtection = 3, + CompleteProtection = 4, + FailsafeProtection = 5 + } + + public enum SiemensOmsAlarmMessageType + { + Invalid = 0, + Alarm = 1, + Notify = 2, + InfoReport = 3, + EventAcknowledgement = 4 + } + + public enum SiemensOmsAlarmJobState + { + Waiting = 0, + Processing = 1, + Distributing = 2 + } + + public enum SiemensOmsAlarmEnableDisableResult + { + Ok = 0, + InvalidRid = 32769, + InvalidAlid = 32770 + } + + public static class SiemensOmsEnumNames + { + public static string AlarmMessageTypeName(int value) => + System.Enum.IsDefined(typeof(SiemensOmsAlarmMessageType), value) + ? ((SiemensOmsAlarmMessageType)value).ToString() + : $"Unknown({value})"; + } +} diff --git a/src/S7CommPlusDriver/Core/SystemEvent.cs b/src/S7CommPlusDriver/Core/SystemEvent.cs index b666ca0..086456b 100644 --- a/src/S7CommPlusDriver/Core/SystemEvent.cs +++ b/src/S7CommPlusDriver/Core/SystemEvent.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * @@ -24,7 +24,7 @@ namespace S7CommPlusDriver /// The 22 byte version may contain a string like "LOGOUT", but this only after a DeleteObject. /// In contrast to all other protocol functions, the values don't use the VLQ encoding! /// - public class SystemEvent + internal class SystemEvent { public byte TransportFlags; public UInt64 ReturnValue; diff --git a/src/S7CommPlusDriver/Core/Utils.cs b/src/S7CommPlusDriver/Core/Utils.cs index 5f41204..0923fa4 100644 --- a/src/S7CommPlusDriver/Core/Utils.cs +++ b/src/S7CommPlusDriver/Core/Utils.cs @@ -1,4 +1,4 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver * diff --git a/src/S7CommPlusDriver/IS7CommPlusSession.cs b/src/S7CommPlusDriver/IS7CommPlusSession.cs new file mode 100644 index 0000000..174c41d --- /dev/null +++ b/src/S7CommPlusDriver/IS7CommPlusSession.cs @@ -0,0 +1,43 @@ +using S7CommPlusDriver.Alarming; +using S7CommPlusDriver.ClientApi; +using System; +using System.Collections.Generic; + +namespace S7CommPlusDriver +{ + internal interface IS7CommPlusSession + { + bool IsConnected { get; } + string LastErrorDetail { get; } + int Connect(S7CommPlusClientOptions options); + int Disconnect(int timeoutMilliseconds); + int CloseTransport(int timeoutMilliseconds); + int Legitimate(string password, string username); + int BrowseVariables(out List variables); + int BrowseBlocks(out List blocks); + int GetPlcStructureXml(out S7CommPlusPlcStructureSnapshot plcStructure); + int GetBlockContent(uint relationId, out S7CommPlusClientBlockContent blockContent); + PlcTag GetPlcTagBySymbol(string symbol); + int GetCpuInfo(out S7CommPlusCpuInfo cpuInfo); + int GetCpuState(out S7CommPlusCpuState cpuState); + int GetCpuCycleTime(out S7CommPlusCpuCycleTime cycleTime); + int GetCpuMemoryUsage(out S7CommPlusCpuMemoryUsage memoryUsage); + int SetCpuOperatingState(int operatingStateRequest); + int GetCpuCultureInfo(out S7CommPlusCpuCultureInfo cultureInfo); + int GetTextLists(IEnumerable languageIds, out S7CommPlusTextListCatalog textLists); + int GetCommunicationResources(out S7CommPlusCommunicationResourceSnapshot resources); + int GetActiveAlarms(out List alarmList, int languageId, Func textListResolver); + int ReadValues(List addresses, out List values, out List errors); + int WriteValues(List addresses, List values, out List errors); + int CreateTagSubscription(List tags, ushort cycleTimeMilliseconds, short initialCreditLimit, out uint subscriptionObjectId); + int WaitForTagSubscriptionNotifications(uint subscriptionObjectId, int timeoutMilliseconds, short creditLimitStep, out List notifications); + int DeleteTagSubscription(uint subscriptionObjectId); + int CreateAlarmSubscription(uint[] languageIds, short initialCreditLimit, out uint subscriptionObjectId); + int WaitForAlarmNotifications(uint subscriptionObjectId, int timeoutMilliseconds, short creditLimitStep, out List notifications); + int DeleteAlarmSubscription(uint subscriptionObjectId); + int CreateTisWatchSubscription(S7CommPlusTisWatchRequest request, out uint subscriptionObjectId); + int WaitForTisWatchNotifications(uint subscriptionObjectId, int timeoutMilliseconds, out List notifications); + string LastTisWatchDiagnostic { get; } + int DeleteTisWatchSubscription(uint subscriptionObjectId); + } +} diff --git a/src/S7CommPlusDriver/Internal/IS7CommPlusProtocolSession.cs b/src/S7CommPlusDriver/Internal/IS7CommPlusProtocolSession.cs new file mode 100644 index 0000000..cf8e94d --- /dev/null +++ b/src/S7CommPlusDriver/Internal/IS7CommPlusProtocolSession.cs @@ -0,0 +1,22 @@ +using System.IO; + +namespace S7CommPlusDriver.Internal +{ + internal interface IS7CommPlusProtocolSession + { + int LastError { get; set; } + int ReadTimeout { get; } + uint SessionId { get; } + uint SessionId2 { get; } + MemoryStream ReceivedPdu { get; } + + int SendFunction(IS7pRequest request); + int SendFunctionAndWait(IS7pRequest request); + void WaitForPdu(int timeoutMilliseconds); + int WaitForNotification(uint subscriptionObjectId, int timeoutMilliseconds, out Notification notification); + int CheckResponse(IS7pRequest request, IS7pResponse response); + int DeleteObject(uint objectId); + void DisconnectTransport(); + void ClearSessionIds(); + } +} diff --git a/src/S7CommPlusDriver/Internal/LegacyAuthenticationFrameKind.cs b/src/S7CommPlusDriver/Internal/LegacyAuthenticationFrameKind.cs new file mode 100644 index 0000000..fc960ed --- /dev/null +++ b/src/S7CommPlusDriver/Internal/LegacyAuthenticationFrameKind.cs @@ -0,0 +1,12 @@ +#if NET8_0_OR_GREATER +namespace S7CommPlusDriver.Internal +{ + internal enum LegacyAuthenticationFrameKind + { + Auto = 0, + S71500HighSession = 1, + S71500LowSessionCompact = 2, + S71500V31Compact = 3 + } +} +#endif diff --git a/src/S7CommPlusDriver/Internal/LegacyChallengeHandshake.cs b/src/S7CommPlusDriver/Internal/LegacyChallengeHandshake.cs new file mode 100644 index 0000000..0a98cf7 --- /dev/null +++ b/src/S7CommPlusDriver/Internal/LegacyChallengeHandshake.cs @@ -0,0 +1,393 @@ +#if NET8_0_OR_GREATER +using HarpoS7; +using HarpoS7.Utilities.Auth; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace S7CommPlusDriver.Internal +{ + internal sealed class LegacyChallengeHandshake + { + private LegacyChallengeHandshake(uint sessionId, string fingerprint, byte[] challenge, EPublicKeyFamily harpoKeyFamily, LegacyOmsPublicKeyFamily omsKeyFamily, string serverSessionVersion, LegacyAuthenticationFrameKind authenticationFrameKind) + { + SessionId = sessionId; + Fingerprint = fingerprint; + Challenge = challenge; + KeyFamily = harpoKeyFamily; + OmsKeyFamily = omsKeyFamily; + ServerSessionVersion = serverSessionVersion; + AuthenticationFrameKind = authenticationFrameKind; + } + + public uint SessionId { get; } + public string Fingerprint { get; } + public byte[] Challenge { get; } + public EPublicKeyFamily KeyFamily { get; } + public LegacyOmsPublicKeyFamily OmsKeyFamily { get; } + public string ServerSessionVersion { get; } + public LegacyAuthenticationFrameKind AuthenticationFrameKind { get; } + + public static bool TryParse(byte[] createObjectPdu, CreateObjectResponse createObjectResponse, out LegacyChallengeHandshake handshake) + { + handshake = null; + if (!TryGetFingerprint(createObjectResponse, out var fingerprint) + && !TryFindFingerprint(createObjectPdu, out fingerprint)) + { + return false; + } + + if (!TryParseFamily(fingerprint, out var harpoKeyFamily, out var omsKeyFamily)) + { + return false; + } + + if (!TryGetChallenge(createObjectResponse, out var challenge) + && !TryFindChallenge(createObjectPdu, out challenge)) + { + return false; + } + + var sessionId = createObjectResponse.ObjectIds[0]; + TryGetServerSessionVersion(createObjectResponse, out var serverSessionVersion); + + var frameKind = SelectAuthenticationFrameKind(createObjectPdu, serverSessionVersion, harpoKeyFamily, sessionId); + handshake = new LegacyChallengeHandshake(sessionId, fingerprint, challenge, harpoKeyFamily, omsKeyFamily, serverSessionVersion, frameKind); + return true; + } + + private const byte AddressArrayValueFlag = 0x20; + + public static SetMultiVariablesRequest CreateAuthenticationRequest( + EPublicKeyFamily keyFamily, + uint sessionId, + ReadOnlySpan publicKeyId, + ReadOnlySpan sessionKeyId, + ReadOnlySpan keyBlob, + ValueStruct serverSessionVersion, + LegacyServerSessionRole serverSessionRole, + LegacyAuthenticationFrameKind frameKind = LegacyAuthenticationFrameKind.Auto) + { + if (serverSessionVersion == null + || publicKeyId.Length != Constants.KeyIdLength + || sessionKeyId.Length != Constants.KeyIdLength + || keyBlob.Length == 0) + { + return null; + } + + var resolvedFrameKind = ResolveAuthenticationFrameKind(keyFamily, sessionId, frameKind); + var request = new SetMultiVariablesRequest(ProtocolVersion.V2) + { + SessionId = sessionId, + SequenceNumber = 2, + InObjectId = sessionId, + WithIntegrityId = false + }; + + request.AddressList.Add(Ids.SessionKey); + request.ValueList.Add(CreateSecurityKey(keyFamily, publicKeyId, sessionKeyId, keyBlob)); + request.AddressList.Add(Ids.ServerSessionVersion); + request.ValueList.Add(serverSessionVersion); + + if (resolvedFrameKind == LegacyAuthenticationFrameKind.S71500HighSession) + { + request.AddressList.Add(Ids.ServerSessionRoles); + request.ValueList.Add(new ValueUIntArray(new ushort[] { (ushort)LegacyServerSessionRole.Hmi }, AddressArrayValueFlag)); + } + else if (resolvedFrameKind == LegacyAuthenticationFrameKind.S71500V31Compact) + { + request.AddressList.Add(Ids.ServerSessionRole); + request.ValueList.Add(new ValueUDInt((uint)serverSessionRole)); + } + else if (keyFamily == EPublicKeyFamily.PlcSim) + { + request.AddressList.Add(Ids.ServerSessionRole); + request.ValueList.Add(new ValueUDInt((uint)serverSessionRole)); + request.AddressList.Add(Ids.LegacyAuthenticationCompatibilityFlag); + request.ValueList.Add(new ValueUDInt(1)); + } + + return request; + } + + private static ValueStruct CreateSecurityKey(EPublicKeyFamily keyFamily, ReadOnlySpan publicKeyId, ReadOnlySpan sessionKeyId, ReadOnlySpan keyBlob) + { + var securityKey = new ValueStruct(Ids.StructSecurityKey); + securityKey.AddStructElement(Ids.SecurityKeyVersion, new ValueUDInt(0)); + securityKey.AddStructElement(Ids.SecurityKeySecurityLevel, new ValueUSInt(0)); + securityKey.AddStructElement(Ids.SecurityKeyPublicKeyId, CreateSecurityKeyId(publicKeyId, (uint)SiemensCsiKeyFlags.GetCommPublicKeyFlags(keyFamily))); + securityKey.AddStructElement(Ids.SecurityKeySymmetricKeyId, CreateSecurityKeyId(sessionKeyId, (uint)SiemensCsiKeyFlags.GetSymmetricKeyFlags(keyFamily))); + securityKey.AddStructElement(Ids.SecurityKeyEncryptedKey, new ValueBlob(0, keyBlob.ToArray())); + return securityKey; + } + + private static ValueStruct CreateSecurityKeyId(ReadOnlySpan keyId, uint flags) + { + var key = new ValueStruct(Ids.SecurityKeyId); + key.AddStructElement(Ids.SecurityKeyIdValue, new ValueULInt(BinaryPrimitives.ReadUInt64LittleEndian(keyId))); + key.AddStructElement(Ids.SecurityKeyIdFlags, new ValueUDInt(flags)); + key.AddStructElement(Ids.SecurityKeyIdInternalFlags, new ValueUDInt(0)); + return key; + } + + private static LegacyAuthenticationFrameKind SelectAuthenticationFrameKind(byte[] createObjectPdu, string serverSessionVersion, EPublicKeyFamily keyFamily, uint sessionId) + { + if (keyFamily != EPublicKeyFamily.S71500) + { + return LegacyAuthenticationFrameKind.Auto; + } + + if (sessionId < LegacyOmsConstants.HighSessionIdStart) + { + return LegacyAuthenticationFrameKind.S71500LowSessionCompact; + } + + if (UsesV31CompactAuthentication(serverSessionVersion) + || (string.IsNullOrEmpty(serverSessionVersion) && ContainsAscii(createObjectPdu, ";V3."))) + { + return LegacyAuthenticationFrameKind.S71500V31Compact; + } + + return LegacyAuthenticationFrameKind.S71500HighSession; + } + + private static bool UsesV31CompactAuthentication(string serverSessionVersion) + { + if (string.IsNullOrEmpty(serverSessionVersion)) + { + return false; + } + + var versionMarker = serverSessionVersion.LastIndexOf(";V", StringComparison.OrdinalIgnoreCase); + if (versionMarker < 0 || versionMarker + 2 >= serverSessionVersion.Length) + { + return false; + } + + var version = serverSessionVersion[(versionMarker + 2)..]; + var dot = version.IndexOf('.'); + if (dot <= 0 || !int.TryParse(version[..dot], out var major)) + { + return false; + } + + return major >= 3 && major < 10; + } + + private static LegacyAuthenticationFrameKind ResolveAuthenticationFrameKind(EPublicKeyFamily keyFamily, uint sessionId, LegacyAuthenticationFrameKind frameKind) + { + if (frameKind != LegacyAuthenticationFrameKind.Auto) + { + return frameKind; + } + + if (keyFamily == EPublicKeyFamily.S71500 && sessionId < LegacyOmsConstants.HighSessionIdStart) + { + return LegacyAuthenticationFrameKind.S71500LowSessionCompact; + } + + if (keyFamily != EPublicKeyFamily.S71500) + { + return LegacyAuthenticationFrameKind.Auto; + } + + return LegacyAuthenticationFrameKind.S71500HighSession; + } + + private static bool ContainsAscii(byte[] data, string value) + { + var needle = Encoding.ASCII.GetBytes(value); + return data.AsSpan().IndexOf(needle) >= 0; + } + + private static int FindSequence(byte[] haystack, byte[] needle) + { + for (var i = 0; i <= haystack.Length - needle.Length; i++) + { + if (haystack.AsSpan(i, needle.Length).SequenceEqual(needle)) + { + return i; + } + } + + return -1; + } + + private static bool TryGetFingerprint(CreateObjectResponse response, out string fingerprint) + { + fingerprint = null; + if (response?.ResponseObject?.Attributes == null + || !response.ResponseObject.Attributes.TryGetValue(Ids.ObjectVariableTypeName, out var value) + || value is not ValueWString name + || string.IsNullOrEmpty(name.GetValue())) + { + return false; + } + + fingerprint = name.GetValue(); + return true; + } + + private static bool TryGetChallenge(CreateObjectResponse response, out byte[] challenge) + { + challenge = null; + if (response?.ResponseObject?.Attributes == null + || !response.ResponseObject.Attributes.TryGetValue(Ids.ServerSessionChallenge, out var value) + || value is not ValueUSIntArray array) + { + return false; + } + + var bytes = array.GetValue(); + if (bytes == null || bytes.Length != LegacyOmsConstants.ChallengeLength) + { + return false; + } + + challenge = new byte[bytes.Length]; + Array.Copy(bytes, challenge, bytes.Length); + return true; + } + + private static bool TryGetServerSessionVersion(CreateObjectResponse response, out string serverSessionVersion) + { + serverSessionVersion = null; + if (response?.ResponseObject?.Attributes == null + || !response.ResponseObject.Attributes.TryGetValue(Ids.ServerSessionVersion, out var value) + || value is not ValueStruct versionStruct) + { + return false; + } + + try + { + if (versionStruct.GetStructElement(Ids.LID_SessionVersionSystemPAOMString) is ValueWString versionString + && !string.IsNullOrWhiteSpace(versionString.GetValue())) + { + serverSessionVersion = versionString.GetValue(); + return true; + } + } + catch (KeyNotFoundException) + { + } + + return false; + } + + private static bool TryFindChallenge(byte[] data, out byte[] challenge) + { + challenge = null; + for (var i = 0; i < data.Length - 6; i++) + { + if (data[i] != ElementID.Attribute) + { + continue; + } + + using var stream = new MemoryStream(data, i + 1, data.Length - i - 1); + S7p.DecodeUInt32Vlq(stream, out var attributeId); + if (attributeId != Ids.ServerSessionChallenge) + { + continue; + } + + var valueOffset = i + 1 + (int)stream.Position; + if (valueOffset + 3 + LegacyOmsConstants.ChallengeLength > data.Length + || data[valueOffset] != 0x10 + || data[valueOffset + 1] != Datatype.USInt + || data[valueOffset + 2] != LegacyOmsConstants.ChallengeLength) + { + continue; + } + + challenge = new byte[LegacyOmsConstants.ChallengeLength]; + Array.Copy(data, valueOffset + 3, challenge, 0, challenge.Length); + return true; + } + + return false; + } + + private static bool TryFindFingerprint(byte[] data, out string fingerprint) + { + fingerprint = null; + for (var i = 0; i <= data.Length - 19; i++) + { + if (!IsDigit(data[i]) || !IsDigit(data[i + 1]) || data[i + 2] != (byte)':') + { + continue; + } + + var length = 3; + while (i + length < data.Length && length < 24 && IsHex(data[i + length])) + { + length++; + } + + if (length >= 19) + { + fingerprint = Encoding.ASCII.GetString(data, i, length); + return true; + } + } + return false; + } + + private static bool TryParseFamily(string fingerprint, out EPublicKeyFamily harpoKeyFamily, out LegacyOmsPublicKeyFamily omsKeyFamily) + { + harpoKeyFamily = default; + omsKeyFamily = default; + if (fingerprint.Length < 2 || !int.TryParse(fingerprint.Substring(0, 2), out var family)) + { + return false; + } + if (!Enum.IsDefined(typeof(EPublicKeyFamily), family)) + { + return false; + } + harpoKeyFamily = (EPublicKeyFamily)family; + if (!TryMapHarpoFamilyToOmsFamily(harpoKeyFamily, out omsKeyFamily)) + { + return false; + } + + return true; + } + + private static bool TryMapHarpoFamilyToOmsFamily(EPublicKeyFamily harpoKeyFamily, out LegacyOmsPublicKeyFamily omsKeyFamily) + { + switch (harpoKeyFamily) + { + case EPublicKeyFamily.S71500: + omsKeyFamily = LegacyOmsPublicKeyFamily.Cpu1500; + return true; + case EPublicKeyFamily.S71200: + omsKeyFamily = LegacyOmsPublicKeyFamily.Cpu1200; + return true; + case EPublicKeyFamily.PlcSim: + omsKeyFamily = LegacyOmsPublicKeyFamily.VPlc; + return true; + default: + omsKeyFamily = default; + return false; + } + } + + private static bool IsDigit(byte value) + { + return value >= (byte)'0' && value <= (byte)'9'; + } + + private static bool IsHex(byte value) + { + return (value >= (byte)'0' && value <= (byte)'9') + || (value >= (byte)'A' && value <= (byte)'F') + || (value >= (byte)'a' && value <= (byte)'f'); + } + } +} +#endif diff --git a/src/S7CommPlusDriver/Internal/LegacyOmsConstants.cs b/src/S7CommPlusDriver/Internal/LegacyOmsConstants.cs new file mode 100644 index 0000000..e1a3fe1 --- /dev/null +++ b/src/S7CommPlusDriver/Internal/LegacyOmsConstants.cs @@ -0,0 +1,87 @@ +namespace S7CommPlusDriver.Internal +{ + internal static class LegacyOmsConstants + { + public const string EngineeringTsap = "SIMATIC-ROOT-ES"; + public const string HmiTsap = "SIMATIC-ROOT-HMI"; + + public const uint HighSessionIdStart = 0x70000000; + public const int ChallengeLength = 20; + public const int PacketDigestLength = 32; + public const int PacketDigestFieldLength = PacketDigestLength + 1; + + public const int ServerSessionRole = 299; + public const int ServerSessionChallenge = 303; + public const int SessionServerChallenge = ServerSessionChallenge; + public const int ServerSessionResponse = 304; + public const int ServerSessionRoles = 305; + public const int ServerSessionClientVersion = 306; + public const int ClientSessionPassword = 309; + public const int ClientSessionLegitimated = 310; + public const int ClientSessionCommunicationFormat = 311; + public const int LidSessionVersionStruct = 314; + public const int LidSessionVersionSystemOms = 315; + public const int LidSessionVersionProjectOms = 316; + public const int LidSessionVersionSystemPaom = 317; + public const int LidSessionVersionProjectPaom = 318; + public const int LidSessionVersionSystemPaomString = 319; + public const int LidSessionVersionProjectPaomString = 320; + public const int LidSessionVersionProjectFormat = 321; + public const int StructSecurityKey = 1800; + public const int SecurityKeyVersion = 1801; + public const int SecurityKeySecurityLevel = 1802; + public const int SecurityKeyPublicKeyId = 1803; + public const int SecurityKeySymmetricKeyId = 1804; + public const int SecurityKeyEncryptedKey = 1805; + public const int EncryptionData = 1811; + public const int StructMac = 1820; + public const int MacAlgorithm = 1821; + public const int MacEncryptedKey = 1822; + public const int MacData = 1823; + public const int SecurityKeyId = 1825; + public const int SecurityKeyIdValue = 1826; + public const int SecurityKeyIdFlags = 1827; + public const int SecurityKeyIdInternalFlags = 1828; + public const int ServerSessionSessionKey = 1830; + public const int SessionKey = ServerSessionSessionKey; + public const int EffectiveProtectionLevel = 1842; + public const int CurrentPlcSecurityLevel = EffectiveProtectionLevel; + public const int ActiveProtectionLevel = 1843; + public const int ExpectedLegitimationLevel = 1844; + public const int CollaborationToken = 1845; + public const int Legitimate = 1846; + public const int LegacySessionSecretResponse = Legitimate; + public const int LegacyAuthenticationCompatibilityFlag = 1902; + } + + internal enum LegacyOmsSecurityType + { + None = 0, + Csi = 2, + Tls = 4 + } + + internal enum LegacyServerSessionRole : byte + { + EngineeringSystem = 1, + Hmi = 2 + } + + internal enum LegacyOmsPublicKeyType + { + CpuPublicKey = 2, + FamilyPublicKey = 4, + CommPublicKey = 16 + } + + internal enum LegacyOmsPublicKeyFamily + { + Cpu1500 = 0x0000, + Cpu1200 = 0x0100, + WinAc = 0x0200, + VPlc = 0x0300, + OpenSsl = 0x8000, + Pms = 0x8200, + NoSecurity = 0xFF00 + } +} diff --git a/src/S7CommPlusDriver/Internal/PlcStructureXmlParser.cs b/src/S7CommPlusDriver/Internal/PlcStructureXmlParser.cs new file mode 100644 index 0000000..161e1ce --- /dev/null +++ b/src/S7CommPlusDriver/Internal/PlcStructureXmlParser.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml.Linq; + +namespace S7CommPlusDriver.Internal +{ + internal static class PlcStructureXmlParser + { + private static readonly Regex EscapedCharRegex = new Regex("_x([0-9A-F]{4})_", RegexOptions.Compiled); + + public static S7CommPlusPlcStructureSnapshot CreateSnapshot(string xml) + { + xml ??= string.Empty; + var structure = Parse(xml); + var marker = CreateProgramChangeMarker(xml, structure); + return new S7CommPlusPlcStructureSnapshot(xml, marker, structure); + } + + public static IReadOnlyList Parse(string xml) + { + if (string.IsNullOrWhiteSpace(xml)) + { + return Array.Empty(); + } + + var document = XDocument.Parse(xml); + var entities = document + .Descendants() + .Where(element => element.Name.LocalName == "Entity") + .Select(element => new { Element = element, Rid = ReadUIntAttribute(element, "Rid") }) + .Where(item => item.Rid.HasValue) + .GroupBy(item => item.Rid.Value) + .ToDictionary(group => group.Key, group => group.First().Element); + + var tagTables = document + .Descendants() + .Where(element => element.Name.LocalName == "TagTable") + .Select(element => new TagTableInfo(ReadAttribute(element, "Name"), ReadDateAttribute(element, "LoadRelevantModifiedTime"))) + .Where(table => !string.IsNullOrWhiteSpace(table.Name)) + .GroupBy(table => table.Name, StringComparer.OrdinalIgnoreCase) + .ToDictionary(group => group.Key, group => group.First(), StringComparer.OrdinalIgnoreCase); + + var target = document + .Descendants() + .FirstOrDefault(element => element.Name.LocalName == "Entity" + && string.Equals(ReadAttribute(element, "Id"), "Target", StringComparison.OrdinalIgnoreCase)); + var unitElement = target? + .Elements() + .FirstOrDefault(element => element.Name.LocalName == "GroupStructureV2")? + .Elements() + .FirstOrDefault(element => element.Name.LocalName == "Unit"); + if (unitElement == null) + { + return Array.Empty(); + } + + var unitChildren = new List + { + ParseSection("Program blocks", unitElement.Element(unitElement.Name.Namespace + "ProgramBlocks"), entities, tagTables), + ParseSection("PLC tags", unitElement.Element(unitElement.Name.Namespace + "PlcTagTables"), entities, tagTables), + ParseSection("PLC data types", unitElement.Element(unitElement.Name.Namespace + "PlcDataTypes"), entities, tagTables) + }; + + return new[] + { + new S7CommPlusPlcStructureNode( + S7CommPlusPlcStructureNodeKind.Unit, + string.IsNullOrWhiteSpace(ReadAttribute(unitElement, "Name")) ? "Default-Unit" : ReadAttribute(unitElement, "Name"), + children: unitChildren) + }; + } + + private static S7CommPlusPlcStructureNode ParseSection( + string name, + XElement section, + IReadOnlyDictionary entities, + IReadOnlyDictionary tagTables) + { + var children = section == null + ? Array.Empty() + : section.Elements().Where(element => element.Name.LocalName == "Group").Select(group => ParseGroup(group, entities, tagTables)).ToArray(); + return new S7CommPlusPlcStructureNode(S7CommPlusPlcStructureNodeKind.Folder, name, children: children); + } + + private static S7CommPlusPlcStructureNode ParseGroup( + XElement group, + IReadOnlyDictionary entities, + IReadOnlyDictionary tagTables) + { + var children = new List(); + foreach (var child in group.Elements()) + { + switch (child.Name.LocalName) + { + case "Group": + children.Add(ParseGroup(child, entities, tagTables)); + break; + case "OnlineId": + children.Add(ParseOnlineId(child.Value, entities, tagTables)); + break; + } + } + + return new S7CommPlusPlcStructureNode( + S7CommPlusPlcStructureNodeKind.Folder, + string.IsNullOrWhiteSpace(ReadAttribute(group, "Name")) ? "Folder" : ReadAttribute(group, "Name"), + children: children); + } + + private static S7CommPlusPlcStructureNode ParseOnlineId( + string onlineId, + IReadOnlyDictionary entities, + IReadOnlyDictionary tagTables) + { + var decoded = ReplaceEscapedChars(onlineId ?? string.Empty); + if (uint.TryParse(decoded, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rid) + && entities.TryGetValue(rid, out var entity)) + { + var header = entity.Elements().FirstOrDefault(element => element.Name.LocalName == "Header"); + if (header != null) + { + return new S7CommPlusPlcStructureNode( + S7CommPlusPlcStructureNodeKind.Block, + ReadAttribute(header, "Name") ?? $"Block {ReadIntAttribute(header, "Number") ?? 0}", + relationId: rid, + number: ReadIntAttribute(header, "Number"), + blockType: NormalizeBlockType(ReadAttribute(header, "Type")), + blockLanguage: NormalizeBlockLanguage(ReadAttribute(header, "ProgrammingLanguage")), + subType: ReadAttribute(header, "SubType"), + lastModified: ReadDateAttribute(header, "LastModified")); + } + } + + if (tagTables.TryGetValue(decoded, out var tagTable)) + { + return new S7CommPlusPlcStructureNode( + S7CommPlusPlcStructureNodeKind.Item, + tagTable.Name, + blockType: "TagTable", + lastModified: tagTable.LastModified); + } + + return new S7CommPlusPlcStructureNode(S7CommPlusPlcStructureNodeKind.Item, decoded); + } + + private static S7CommPlusProgramChangeMarker CreateProgramChangeMarker(string xml, IReadOnlyList structure) + { + var nodes = EnumerateNodes(structure ?? Array.Empty()).ToArray(); + var lastModified = nodes + .Where(node => node.LastModified.HasValue) + .Select(node => node.LastModified.Value) + .DefaultIfEmpty() + .Max(); + var hasLastModified = nodes.Any(node => node.LastModified.HasValue); + return new S7CommPlusProgramChangeMarker( + ComputeSha256Hex(xml ?? string.Empty), + hasLastModified ? lastModified : (DateTime?)null, + nodes.Count(node => node.Kind == S7CommPlusPlcStructureNodeKind.Block), + nodes.Count(node => string.Equals(node.BlockType, "TagTable", StringComparison.OrdinalIgnoreCase))); + } + + private static IEnumerable EnumerateNodes(IEnumerable nodes) + { + foreach (var node in nodes) + { + yield return node; + foreach (var child in EnumerateNodes(node.Children)) + { + yield return child; + } + } + } + + private static string ComputeSha256Hex(string value) + { + using var sha = SHA256.Create(); + var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(value)); + return BitConverter.ToString(hash).Replace("-", string.Empty); + } + + private static string ReplaceEscapedChars(string text) + { + return EscapedCharRegex.Replace(text, match => ((char)int.Parse(match.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture)).ToString()); + } + + private static string ReadAttribute(XElement element, string name) + { + return element.Attribute(name)?.Value; + } + + private static uint? ReadUIntAttribute(XElement element, string name) + { + return uint.TryParse(ReadAttribute(element, name), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : (uint?)null; + } + + private static int? ReadIntAttribute(XElement element, string name) + { + return int.TryParse(ReadAttribute(element, name), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : (int?)null; + } + + private static DateTime? ReadDateAttribute(XElement element, string name) + { + return DateTime.TryParse(ReadAttribute(element, name), CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var value) ? value : (DateTime?)null; + } + + private static string NormalizeBlockType(string value) + { + if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var numeric)) + { + return value; + } + + return numeric switch + { + 1 => "UDT", + 2 => "Enum", + 8 => "OB", + 10 => "DB", + 11 => "SDB", + 12 => "FC", + 13 => "SFC", + 14 => "FB", + 15 => "SFB", + 16 => "CB", + 17 => "MTH", + 257 => "FBT", + 258 => "SDT", + _ => value + }; + } + + private static string NormalizeBlockLanguage(string value) + { + if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var numeric)) + { + return value; + } + + return Enum.IsDefined(typeof(S7CommPlusProgrammingLanguage), numeric) + ? ((S7CommPlusProgrammingLanguage)numeric).ToString() + : value; + } + + private sealed class TagTableInfo + { + public TagTableInfo(string name, DateTime? lastModified) + { + Name = name; + LastModified = lastModified; + } + + public string Name { get; } + public DateTime? LastModified { get; } + } + } +} diff --git a/src/S7CommPlusDriver/Internal/S7CommPlusErrorClassifier.cs b/src/S7CommPlusDriver/Internal/S7CommPlusErrorClassifier.cs new file mode 100644 index 0000000..5d748a6 --- /dev/null +++ b/src/S7CommPlusDriver/Internal/S7CommPlusErrorClassifier.cs @@ -0,0 +1,75 @@ +namespace S7CommPlusDriver.Internal +{ + internal static class S7CommPlusErrorClassifier + { + public static S7CommPlusException CreateException(string operation, string endpoint, int errorCode) + { + var message = $"{operation} failed for PLC {endpoint}: {S7Client.ErrorText(errorCode)}."; + return CreateTypedException(operation, endpoint, errorCode, message); + } + + public static S7CommPlusException CreateException(string operation, string endpoint, int errorCode, string detail) + { + var message = $"{operation} failed for PLC {endpoint}: {S7Client.ErrorText(errorCode)}."; + if (!string.IsNullOrWhiteSpace(detail)) + { + message += " " + detail.Trim(); + } + + return CreateTypedException(operation, endpoint, errorCode, message); + } + + private static S7CommPlusException CreateTypedException(string operation, string endpoint, int errorCode, string message) + { + if (operation == "Legitimate") + { + return new S7CommPlusLegitimationException(operation, endpoint, errorCode, IsTransient(errorCode), message); + } + + if (IsTisWatchCreateRejection(operation, errorCode)) + { + message += " The PLC rejected opening the TIS online block view. Known causes are a block/view that is already watched by TIA or a stale CPU-side watch job; for FBs it can also mean the trigger needs a more specific instance/call path."; + return new S7CommPlusTisWatchUnavailableException(operation, endpoint, errorCode, IsTransient(errorCode), message); + } + + return new S7CommPlusConnectionException(operation, endpoint, errorCode, IsTransient(errorCode), message); + } + + private static bool IsTisWatchCreateRejection(string operation, int errorCode) + { + return errorCode == S7Consts.errCliInvalidParams + && operation != null + && operation.Contains("CreateTisWatchSubscription") + && operation.Contains("create TIS watch job"); + } + + public static bool IsTransient(int errorCode) + { + return errorCode == S7Consts.errTCPConnectionTimeout + || errorCode == S7Consts.errTCPConnectionFailed + || errorCode == S7Consts.errTCPReceiveTimeout + || errorCode == S7Consts.errTCPDataReceive + || errorCode == S7Consts.errTCPSendTimeout + || errorCode == S7Consts.errTCPDataSend + || errorCode == S7Consts.errTCPConnectionReset + || errorCode == S7Consts.errTCPNotConnected + || errorCode == S7Consts.errTCPUnreachableHost + || errorCode == S7Consts.errCliJobTimeout + || errorCode == S7Consts.errOpenSSL + || errorCode == S7Consts.errS7CommPlusDigestMismatch; + } + + public static bool IsConnectionDefinitelyClosed(int errorCode) + { + return errorCode == S7Consts.errTCPNotConnected + || errorCode == S7Consts.errTCPConnectionReset + || IsIsoInvalidPdu(errorCode); + } + + private static bool IsIsoInvalidPdu(int errorCode) + { + return errorCode >= S7Consts.errIsoInvalidPDU + && errorCode <= S7Consts.errIsoInvalidPDU12; + } + } +} diff --git a/src/S7CommPlusDriver/Internal/S7CommPlusProtocolConstants.cs b/src/S7CommPlusDriver/Internal/S7CommPlusProtocolConstants.cs new file mode 100644 index 0000000..2093ba3 --- /dev/null +++ b/src/S7CommPlusDriver/Internal/S7CommPlusProtocolConstants.cs @@ -0,0 +1,56 @@ +namespace S7CommPlusDriver.Internal +{ + internal static class S7CommPlusProtocolConstants + { + public const byte FrameMarker = 0x72; + public const int DefaultIsoTpduSize = 1024; + public const int TpktHeaderLength = 4; + public const int CotpHeaderLength = 3; + public const int S7CommPlusHeaderLength = 4; + public const int S7CommPlusTrailerLength = 4; + public const int TlsRecordHeaderLength = 5; + public const int TlsAesGcmRecordOverhead = 17; + public const int MaxCotpParameterLength = 255; + + public const byte RequestWithResponseTransportFlags = 0x34; + public const byte CreateObjectTransportFlags = 0x36; + public const byte FireAndForgetTransportFlags = 0x74; + public const byte ValueArrayFlag = 0x10; + public const byte ValueAddressArrayFlag = 0x20; + public const ushort GetVarSubstreamedRequestUnknown1 = 0x0001; + + public const uint SubscriptionRelationIdStart = 0x7fffc001; + public const uint AlarmSubscriptionRefRelationIdStart = 0x51010001; + public const uint TisSubscriptionRefRelationIdStart = 0x51020001; + public const short DefaultSubscriptionCreditLimit = 10; + public const short DefaultSubscriptionCreditLimitStep = 5; + public const ushort SubscriptionTicksUnlimited = 65535; + public const int SubscriptionDefaultAttribute1055 = 1055; + public const uint SubscriptionListCreateFlag = 0x80000000; + public const uint SubscriptionItemAddressHeaderFlag = 0x80040000; + public const uint AlarmSubscriptionReferenceListHeader = 0x80010000; + public const ushort AlarmDomainAll = 65535; + public const byte AlarmSubscriptionTriggerAndTransmitMode = 3; + public const string AlarmSubscriptionName = "S7pDriver_Alarming"; + + public const int SystemLimitPlcSubscriptions = 0; + public const int SystemLimitPlcAttributes = 1; + public const int SystemLimitSubscriptionMemory = 2; + public const int SystemLimitTagsPerReadRequest = 1000; + public const int SystemLimitTagsPerWriteRequest = 1001; + } + + internal enum SubscriptionFunctionClass : byte + { + Variables = 0, + Tis = 1, + Alarms = 2 + } + + internal enum SubscriptionRouteMode : byte + { + Alarm = 0x02, + Tis = 0x01, + CyclicAndChangedValues = 0x14 + } +} diff --git a/src/S7CommPlusDriver/Internal/S7CommPlusProtocolRequests.cs b/src/S7CommPlusDriver/Internal/S7CommPlusProtocolRequests.cs new file mode 100644 index 0000000..ee465f3 --- /dev/null +++ b/src/S7CommPlusDriver/Internal/S7CommPlusProtocolRequests.cs @@ -0,0 +1,211 @@ +using System.Collections.Generic; + +namespace S7CommPlusDriver.Internal +{ + internal sealed class S7CommPlusProtocolRequests + { + private readonly IS7CommPlusProtocolSession _session; + + public S7CommPlusProtocolRequests(IS7CommPlusProtocolSession session) + { + _session = session; + } + + public int Explore( + uint relationId, + IEnumerable attributes, + out ExploreResponse response, + byte exploreChildsRecursive = 1, + byte exploreParents = 0) + { + response = null; + + var request = new ExploreRequest(ProtocolVersion.V2) + { + ExploreId = relationId, + ExploreRequestId = Ids.None, + ExploreChildsRecursive = exploreChildsRecursive, + ExploreParents = exploreParents + }; + + if (attributes != null) + { + request.AddressList.AddRange(attributes); + } + + return SendExplore(request, out response); + } + + public int SendExplore(ExploreRequest request, out ExploreResponse response) + { + response = null; + var result = SendAndReceive(request); + if (result != 0) + { + return result; + } + + response = ExploreResponse.DeserializeFromPdu(_session.ReceivedPdu, true); + return _session.CheckResponse(request, response); + } + + public int GetVarSubstreamed(uint objectId, ushort address, out PValue value) + { + value = null; + + var request = new GetVarSubstreamedRequest(ProtocolVersion.V2) + { + InObjectId = objectId, + SessionId = _session.SessionId, + Address = address + }; + + var result = SendAndReceive(request); + if (result != 0) + { + return result; + } + + var response = GetVarSubstreamedResponse.DeserializeFromPdu(_session.ReceivedPdu); + if (response == null) + { + return S7Consts.errIsoInvalidPDU8; + } + + value = response.Value; + return 0; + } + + public int GetVariable(uint objectId, uint address, out PValue value) + { + value = null; + + var request = new GetVariableRequest(ProtocolVersion.V2) + { + InObjectId = objectId, + SessionId = _session.SessionId, + Address = address + }; + + var result = SendAndReceive(request); + if (result != 0) + { + return result; + } + + var response = GetVariableResponse.DeserializeFromPdu(_session.ReceivedPdu); + if (response == null) + { + return S7Consts.errIsoInvalidPDU8; + } + + if (response.ReturnValue != 0 || response.Value == null) + { + return S7Consts.errIsoInvalidPDU8; + } + + value = response.Value; + return 0; + } + + public int CreateObject(CreateObjectRequest request, out CreateObjectResponse response) + { + response = null; + var result = SendAndReceive(request); + if (result != 0) + { + return result; + } + + response = CreateObjectResponse.DeserializeFromPdu(_session.ReceivedPdu); + return response == null ? S7Consts.errIsoInvalidPDU : 0; + } + + public int WaitNotification(int timeoutMilliseconds, out Notification notification) + { + return WaitNotification(0, timeoutMilliseconds, out notification); + } + + public int WaitNotification(uint subscriptionObjectId, int timeoutMilliseconds, out Notification notification) + { + notification = null; + var result = _session.WaitForNotification(subscriptionObjectId, timeoutMilliseconds, out notification); + if (result != 0) + { + return result; + } + + return notification == null ? S7Consts.errIsoInvalidPDU : 0; + } + + public int SetSubscriptionCreditLimit(uint subscriptionObjectId, short limit) + { + if (subscriptionObjectId == 0) + { + return S7Consts.errCliInvalidParams; + } + + var request = new SetVariableRequest(ProtocolVersion.V2) + { + TransportFlags = S7CommPlusProtocolConstants.FireAndForgetTransportFlags, + InObjectId = subscriptionObjectId, + Address = Ids.SubscriptionCreditLimit, + Value = new ValueInt(limit) + }; + + return _session.SendFunction(request); + } + + public int SetVariable(uint objectId, uint address, PValue value) + { + if (objectId == 0 || value == null) + { + return S7Consts.errCliInvalidParams; + } + + var request = new SetVariableRequest(ProtocolVersion.V2) + { + TransportFlags = S7CommPlusProtocolConstants.FireAndForgetTransportFlags, + InObjectId = objectId, + Address = address, + Value = value + }; + + return _session.SendFunction(request); + } + + public int SetMultiVariablesRaw(uint inObjectId, IEnumerable addressFields, IEnumerable values) + { + if (addressFields == null || values == null) + { + return S7Consts.errCliInvalidParams; + } + + var request = new SetMultiVariablesRequest(ProtocolVersion.V2) + { + InObjectId = inObjectId, + UseRawAddressList = true + }; + request.AddressList.AddRange(addressFields); + request.ValueList.AddRange(values); + if (request.ValueList.Count == 0) + { + return S7Consts.errCliInvalidParams; + } + + var result = SendAndReceive(request); + if (result != 0) + { + return result; + } + + var response = SetMultiVariablesResponse.DeserializeFromPdu(_session.ReceivedPdu); + return response == null ? S7Consts.errIsoInvalidPDU : _session.CheckResponse(request, response); + } + + private int SendAndReceive(IS7pRequest request) + { + return _session.SendFunctionAndWait(request); + } + } +} diff --git a/src/S7CommPlusDriver/Internal/S7CommPlusSymbolCrc.cs b/src/S7CommPlusDriver/Internal/S7CommPlusSymbolCrc.cs new file mode 100644 index 0000000..39298f2 --- /dev/null +++ b/src/S7CommPlusDriver/Internal/S7CommPlusSymbolCrc.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace S7CommPlusDriver.Internal +{ + internal static class S7CommPlusSymbolCrc + { + private const uint Polynomial = 0xF4ACFB13; + private const byte ArrayTypeCode = 0x10; + private const byte StructChildDelimiter = 0x89; + + private static readonly uint[] Table = CreateTable(); + + internal readonly struct PathSegment + { + private PathSegment(string memberName, uint softdatatype, bool isArray, uint elementSoftdatatype, int lowerBound) + { + MemberName = memberName ?? string.Empty; + Softdatatype = softdatatype; + IsArray = isArray; + ElementSoftdatatype = elementSoftdatatype; + LowerBound = lowerBound; + } + + public string MemberName { get; } + public uint Softdatatype { get; } + public bool IsArray { get; } + public uint ElementSoftdatatype { get; } + public int LowerBound { get; } + + public static PathSegment Member(string memberName, uint softdatatype) + { + return new PathSegment(memberName, softdatatype, false, 0, 0); + } + + public static PathSegment Array(string memberName, uint elementSoftdatatype, int lowerBound) + { + return new PathSegment(memberName, global::S7CommPlusDriver.Softdatatype.S7COMMP_SOFTDATATYPE_ARRAY, true, elementSoftdatatype, lowerBound); + } + } + + public static uint ComputeItemCrc(string memberName, uint softdatatype) + { + return FinalizeItemCrc(MemberInnerCrc(memberName, MapSoftdatatypeToTypeCode(softdatatype))); + } + + public static uint ComputeArrayItemCrc(string memberName, uint elementSoftdatatype, int lowerBound) + { + return FinalizeItemCrc(MemberInnerCrc(memberName, ArrayTypeCode, MapSoftdatatypeToTypeCode(elementSoftdatatype), lowerBound)); + } + + public static uint ComputeFromSegments(IReadOnlyList pathSegments) + { + if (pathSegments == null || pathSegments.Count == 0) + { + return 0; + } + + if (pathSegments.Count == 1) + { + var segment = pathSegments[0]; + return segment.IsArray + ? ComputeArrayItemCrc(segment.MemberName, segment.ElementSoftdatatype, segment.LowerBound) + : ComputeItemCrc(segment.MemberName, segment.Softdatatype); + } + + var crc = new Crc32(); + crc.UpdateUInt32LittleEndian(MemberInnerCrc(pathSegments[0])); + for (var i = 1; i < pathSegments.Count; i++) + { + crc.UpdateByte(StructChildDelimiter); + crc.UpdateUInt32LittleEndian(MemberInnerCrc(pathSegments[i])); + } + return crc.Value; + } + + private static uint MemberInnerCrc(PathSegment segment) + { + return segment.IsArray + ? MemberInnerCrc(segment.MemberName, ArrayTypeCode, MapSoftdatatypeToTypeCode(segment.ElementSoftdatatype), segment.LowerBound) + : MemberInnerCrc(segment.MemberName, MapSoftdatatypeToTypeCode(segment.Softdatatype)); + } + + private static uint MemberInnerCrc(string memberName, byte typeCode) + { + var crc = new Crc32(); + crc.UpdateString(memberName); + crc.UpdateByte(typeCode); + return crc.Value; + } + + private static uint MemberInnerCrc(string memberName, byte typeCode, byte elementTypeCode, int lowerBound) + { + var crc = new Crc32(); + crc.UpdateString(memberName); + crc.UpdateByte(typeCode); + crc.UpdateByte(elementTypeCode); + crc.UpdateInt32LittleEndian(lowerBound); + return crc.Value; + } + + private static uint FinalizeItemCrc(uint innerCrc) + { + var crc = new Crc32(); + crc.UpdateUInt32LittleEndian(innerCrc); + return crc.Value; + } + + private static byte MapSoftdatatypeToTypeCode(uint softdatatype) + { + switch (softdatatype) + { + case Softdatatype.S7COMMP_SOFTDATATYPE_BBOOL: + return (byte)Softdatatype.S7COMMP_SOFTDATATYPE_BOOL; + + case Softdatatype.S7COMMP_SOFTDATATYPE_AOMIDENT: + case Softdatatype.S7COMMP_SOFTDATATYPE_EVENTANY: + case Softdatatype.S7COMMP_SOFTDATATYPE_EVENTATT: + case Softdatatype.S7COMMP_SOFTDATATYPE_FOLDER: + case Softdatatype.S7COMMP_SOFTDATATYPE_AOMAID: + case Softdatatype.S7COMMP_SOFTDATATYPE_AOMLINK: + case Softdatatype.S7COMMP_SOFTDATATYPE_EVENTHWINT: + case Softdatatype.S7COMMP_SOFTDATATYPE_CONNRID: + return (byte)Softdatatype.S7COMMP_SOFTDATATYPE_DWORD; + + case Softdatatype.S7COMMP_SOFTDATATYPE_HWANY: + case Softdatatype.S7COMMP_SOFTDATATYPE_HWIOSYSTEM: + case Softdatatype.S7COMMP_SOFTDATATYPE_HWDPMASTER: + case Softdatatype.S7COMMP_SOFTDATATYPE_HWDEVICE: + case Softdatatype.S7COMMP_SOFTDATATYPE_HWDPSLAVE: + case Softdatatype.S7COMMP_SOFTDATATYPE_HWIO: + case Softdatatype.S7COMMP_SOFTDATATYPE_HWMODULE: + case Softdatatype.S7COMMP_SOFTDATATYPE_HWSUBMODULE: + case Softdatatype.S7COMMP_SOFTDATATYPE_HWHSC: + case Softdatatype.S7COMMP_SOFTDATATYPE_HWPWM: + case Softdatatype.S7COMMP_SOFTDATATYPE_HWPTO: + case Softdatatype.S7COMMP_SOFTDATATYPE_HWINTERFACE: + case Softdatatype.S7COMMP_SOFTDATATYPE_HWIEPORT: + case Softdatatype.S7COMMP_SOFTDATATYPE_CONNANY: + case Softdatatype.S7COMMP_SOFTDATATYPE_CONNPRG: + case Softdatatype.S7COMMP_SOFTDATATYPE_CONNOUC: + return (byte)Softdatatype.S7COMMP_SOFTDATATYPE_WORD; + + case Softdatatype.S7COMMP_SOFTDATATYPE_OBANY: + case Softdatatype.S7COMMP_SOFTDATATYPE_OBDELAY: + case Softdatatype.S7COMMP_SOFTDATATYPE_OBTOD: + case Softdatatype.S7COMMP_SOFTDATATYPE_OBCYCLIC: + case Softdatatype.S7COMMP_SOFTDATATYPE_OBATT: + case Softdatatype.S7COMMP_SOFTDATATYPE_OBPCYCLE: + case Softdatatype.S7COMMP_SOFTDATATYPE_OBHWINT: + case Softdatatype.S7COMMP_SOFTDATATYPE_OBDIAG: + case Softdatatype.S7COMMP_SOFTDATATYPE_OBTIMEERROR: + case Softdatatype.S7COMMP_SOFTDATATYPE_OBSTARTUP: + return (byte)Softdatatype.S7COMMP_SOFTDATATYPE_INT; + + case Softdatatype.S7COMMP_SOFTDATATYPE_PORT: + case Softdatatype.S7COMMP_SOFTDATATYPE_RTM: + case Softdatatype.S7COMMP_SOFTDATATYPE_PIP: + case Softdatatype.S7COMMP_SOFTDATATYPE_DBANY: + case Softdatatype.S7COMMP_SOFTDATATYPE_DBWWW: + case Softdatatype.S7COMMP_SOFTDATATYPE_DBDYN: + return (byte)Softdatatype.S7COMMP_SOFTDATATYPE_UINT; + + default: + return (byte)softdatatype; + } + } + + private static uint[] CreateTable() + { + var table = new uint[256]; + for (uint i = 0; i < table.Length; i++) + { + var crc = i << 24; + for (var bit = 0; bit < 8; bit++) + { + crc = (crc & 0x80000000) != 0 + ? unchecked((crc << 1) ^ Polynomial) + : crc << 1; + } + table[i] = crc; + } + return table; + } + + private struct Crc32 + { + private uint _state; + + public uint Value => _state; + + public void UpdateString(string value) + { + var bytes = Encoding.UTF8.GetBytes(value ?? string.Empty); + for (var i = 0; i < bytes.Length; i++) + { + UpdateByte(bytes[i]); + } + } + + public void UpdateInt32LittleEndian(int value) + { + UpdateByte((byte)value); + UpdateByte((byte)(value >> 8)); + UpdateByte((byte)(value >> 16)); + UpdateByte((byte)(value >> 24)); + } + + public void UpdateUInt32LittleEndian(uint value) + { + UpdateByte((byte)value); + UpdateByte((byte)(value >> 8)); + UpdateByte((byte)(value >> 16)); + UpdateByte((byte)(value >> 24)); + } + + public void UpdateByte(byte value) + { + _state = unchecked(Table[(value ^ (_state >> 24)) & 0xFF] ^ (_state << 8)); + } + } + } +} diff --git a/src/S7CommPlusDriver/Legitimation/Legitimation.cs b/src/S7CommPlusDriver/Legitimation/Legitimation.cs index 2597d7c..a69ab9b 100644 --- a/src/S7CommPlusDriver/Legitimation/Legitimation.cs +++ b/src/S7CommPlusDriver/Legitimation/Legitimation.cs @@ -1,4 +1,4 @@ -using S7CommPlusDriver.Legitimation; +using S7CommPlusDriver.Legitimation; using System; using System.Collections.Generic; using System.IO; @@ -10,7 +10,7 @@ namespace S7CommPlusDriver { - public partial class S7CommPlusConnection + internal partial class S7CommPlusProtocolSession { private byte[] omsSecret; @@ -32,17 +32,17 @@ private int legitimate(ValueStruct serverSession, string password, string userna // doc: https://cache.industry.siemens.com/dl/files/068/109769068/att_1329908/v4/109769068_UsingCertificatesWithTIAPortal_DOC_V2_1_en.pdf // Certificates in the scope of PG/PC and HMI communication // Starting with TIA Portal V17, PG / PC and HMI communication is secured with TLS, protecting the data exchanged between - // Field PGs and HMIs with SIMATIC CPUs. + // Field PGs and HMIs with SIMATIC CPUs. // The CPU families that support Secure PG / HMI communication are: - // • S7 - 1500 controllers as of firmware version V2.9. - // • S7 - 1200 controllers as of firmware version V4.5. - // • Software controllers as of firmware version V21.9. - // • SIMATIC Drive controllers as of firmware version V2.9. - // • PLCSim and PLCSim Advanced Version V4.0. + // � S7 - 1500 controllers as of firmware version V2.9. + // � S7 - 1200 controllers as of firmware version V4.5. + // � Software controllers as of firmware version V21.9. + // � SIMATIC Drive controllers as of firmware version V2.9. + // � PLCSim and PLCSim Advanced Version V4.0. // HMI components that support Secure PG/ HMI communication, as of image version V17, are: - // • Panels or PCs configured with WinCC Basic, Comfort and Advanced. - // • PCs with WinCC RT Professional. - // • WinCC Unified PCs and Comfort Panels. + // � Panels or PCs configured with WinCC Basic, Comfort and Advanced. + // � PCs with WinCC RT Professional. + // � WinCC Unified PCs and Comfort Panels. // In addition, SINAMICS RT SW, as of version V6.1, and STARTDRIVE, as of version V17, support secure communication string sessionVersionPAOMString = ((ValueWString)serverSession.GetStructElement((uint)Ids.LID_SessionVersionSystemPAOMString)).GetValue(); var reVersions = new Regex( @@ -52,7 +52,7 @@ private int legitimate(ValueStruct serverSession, string password, string userna Match m = reVersions.Match(sessionVersionPAOMString); if (!m.Success) { - Console.WriteLine("S7CommPlusConnection - Legitimate: Could not extract firmware version!"); + System.Diagnostics.Trace.WriteLine("S7CommPlusProtocolSession - Legitimate: Could not extract firmware version!"); return S7Consts.errCliFirmwareNotSupported; } string deviceVersion = m.Groups[1].Value; // e.g., "672" @@ -64,7 +64,7 @@ private int legitimate(ValueStruct serverSession, string password, string userna var parts = firmwareVersion.Split('.'); if (parts.Length < 2 || !int.TryParse(parts[0], out var major) || !int.TryParse(parts[1], out var minor)) { - Console.WriteLine($"S7CommPlusConnection - Legitimate: Invalid firmware format: {firmwareVersion}"); + System.Diagnostics.Trace.WriteLine($"S7CommPlusProtocolSession - Legitimate: Invalid firmware format: {firmwareVersion}"); return S7Consts.errCliFirmwareNotSupported; } fwVerNo = (major * 100) + minor; @@ -76,7 +76,7 @@ private int legitimate(ValueStruct serverSession, string password, string userna { if (fwVerNo < 209) { - Console.WriteLine("S7CommPlusConnection - Legitimate: Firmware version is not supported!"); + System.Diagnostics.Trace.WriteLine("S7CommPlusProtocolSession - Legitimate: Firmware version is not supported!"); return S7Consts.errCliFirmwareNotSupported; } if (fwVerNo < 301) @@ -92,7 +92,7 @@ private int legitimate(ValueStruct serverSession, string password, string userna { if (fwVerNo < 403) { - Console.WriteLine("S7CommPlusConnection - Legitimate: Firmware version is not supported!"); + System.Diagnostics.Trace.WriteLine("S7CommPlusProtocolSession - Legitimate: Firmware version is not supported!"); return S7Consts.errCliFirmwareNotSupported; } if (fwVerNo < 407) @@ -104,14 +104,14 @@ private int legitimate(ValueStruct serverSession, string password, string userna { if (fwVerNo < 2109) { - Console.WriteLine("S7CommPlusConnection - Legitimate: Firmware version is not supported!"); + System.Diagnostics.Trace.WriteLine("S7CommPlusProtocolSession - Legitimate: Firmware version is not supported!"); return S7Consts.errCliFirmwareNotSupported; } legacyLegitimation = true; } else { - Console.WriteLine("S7CommPlusConnection - Legitimate: Device version is not supported!"); + System.Diagnostics.Trace.WriteLine("S7CommPlusProtocolSession - Legitimate: Device version is not supported!"); return S7Consts.errCliDeviceNotSupported; } @@ -120,24 +120,17 @@ private int legitimate(ValueStruct serverSession, string password, string userna getVarSubstreamedReq.InObjectId = m_SessionId; getVarSubstreamedReq.SessionId = m_SessionId; getVarSubstreamedReq.Address = Ids.EffectiveProtectionLevel; - int res = SendS7plusFunctionObject(getVarSubstreamedReq); + int res = SendS7plusFunctionObjectAndWait(getVarSubstreamedReq, m_ReadTimeout); if (res != 0) { m_client.Disconnect(); return res; } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - m_client.Disconnect(); - return m_LastError; - } var getVarSubstreamedRes = GetVarSubstreamedResponse.DeserializeFromPdu(m_ReceivedPDU); if (getVarSubstreamedRes == null) { - Console.WriteLine("S7CommPlusConnection - Legitimate: GetVarSubstreamedResponse with Error!"); + System.Diagnostics.Trace.WriteLine("S7CommPlusProtocolSession - Legitimate: GetVarSubstreamedResponse with Error!"); m_client.Disconnect(); return S7Consts.errIsoInvalidPDU; } @@ -159,7 +152,7 @@ private int legitimate(ValueStruct serverSession, string password, string userna } else if (accessLevel > AccessLevel.FullAccess) { - Console.WriteLine("S7CommPlusConnection - Legitimate: Warning: Access level is not fullaccess but no password set!"); + System.Diagnostics.Trace.WriteLine("S7CommPlusProtocolSession - Legitimate: Warning: Access level is not fullaccess but no password set!"); } return 0; @@ -178,24 +171,17 @@ private int legitimateNew(string password, string username = "") getVarSubstreamedReq_challange.InObjectId = m_SessionId; getVarSubstreamedReq_challange.SessionId = m_SessionId; getVarSubstreamedReq_challange.Address = Ids.ServerSessionRequest; - int res = SendS7plusFunctionObject(getVarSubstreamedReq_challange); + int res = SendS7plusFunctionObjectAndWait(getVarSubstreamedReq_challange, m_ReadTimeout); if (res != 0) { m_client.Disconnect(); return res; } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - m_client.Disconnect(); - return m_LastError; - } var getVarSubstreamedRes_challenge = GetVarSubstreamedResponse.DeserializeFromPdu(m_ReceivedPDU); if (getVarSubstreamedRes_challenge == null) { - Console.WriteLine("S7CommPlusConnection - Legitimate: getVarSubstreamedRes_challenge with Error!"); + System.Diagnostics.Trace.WriteLine("S7CommPlusProtocolSession - Legitimate: getVarSubstreamedRes_challenge with Error!"); m_client.Disconnect(); return S7Consts.errIsoInvalidPDU; } @@ -224,24 +210,17 @@ private int legitimateNew(string password, string username = "") setVariableReq.SessionId = m_SessionId; setVariableReq.Address = Ids.Legitimate; setVariableReq.Value = new ValueBlob(0, challengeResponse); - res = SendS7plusFunctionObject(setVariableReq); + res = SendS7plusFunctionObjectAndWait(setVariableReq, m_ReadTimeout); if (res != 0) { m_client.Disconnect(); return res; } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - m_client.Disconnect(); - return m_LastError; - } var setVariableResponse = SetVariableResponse.DeserializeFromPdu(m_ReceivedPDU); if (setVariableResponse == null) { - Console.WriteLine("S7CommPlusConnection - Legitimate: setVariableResponse with Error!"); + System.Diagnostics.Trace.WriteLine("S7CommPlusProtocolSession - Legitimate: setVariableResponse with Error!"); m_client.Disconnect(); return S7Consts.errIsoInvalidPDU; } @@ -249,7 +228,7 @@ private int legitimateNew(string password, string username = "") Int16 errorCode = (Int16)setVariableResponse.ReturnValue; if (errorCode < 0) { - Console.WriteLine("S7CommPlusConnection - Legitimate: access denied"); + System.Diagnostics.Trace.WriteLine("S7CommPlusProtocolSession - Legitimate: access denied"); m_client.Disconnect(); return S7Consts.errCliAccessDenied; } @@ -281,7 +260,7 @@ private static byte[] buildLegitimationPayload(string password, string username // Login with only password = legacy login // Hash password byte[] hashedPw; - using (SHA1Managed sha1 = new SHA1Managed()) + using (SHA1 sha1 = SHA1.Create()) { hashedPw = sha1.ComputeHash(Encoding.UTF8.GetBytes(password)); } @@ -310,24 +289,17 @@ private int legitimateLegacy(string password) getVarSubstreamedReq_challange.InObjectId = m_SessionId; getVarSubstreamedReq_challange.SessionId = m_SessionId; getVarSubstreamedReq_challange.Address = Ids.ServerSessionRequest; - int res = SendS7plusFunctionObject(getVarSubstreamedReq_challange); + int res = SendS7plusFunctionObjectAndWait(getVarSubstreamedReq_challange, m_ReadTimeout); if (res != 0) { m_client.Disconnect(); return res; } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - m_client.Disconnect(); - return m_LastError; - } var getVarSubstreamedRes_challenge = GetVarSubstreamedResponse.DeserializeFromPdu(m_ReceivedPDU); if (getVarSubstreamedRes_challenge == null) { - Console.WriteLine("S7CommPlusConnection - Legitimate: getVarSubstreamedRes_challenge with Error!"); + System.Diagnostics.Trace.WriteLine("S7CommPlusProtocolSession - Legitimate: getVarSubstreamedRes_challenge with Error!"); m_client.Disconnect(); return S7Consts.errIsoInvalidPDU; } @@ -336,13 +308,13 @@ private int legitimateLegacy(string password) // Calculate challengeResponse [sha1(password) xor challenge] byte[] challengeResponse; - using (SHA1Managed sha1 = new SHA1Managed()) + using (SHA1 sha1 = SHA1.Create()) { challengeResponse = sha1.ComputeHash(Encoding.UTF8.GetBytes(password)); } if (challengeResponse.Length != challenge.Length) { - Console.WriteLine("S7CommPlusConnection - Legitimate: challengeResponse.Length != challenge.Length"); + System.Diagnostics.Trace.WriteLine("S7CommPlusProtocolSession - Legitimate: challengeResponse.Length != challenge.Length"); m_client.Disconnect(); return S7Consts.errIsoInvalidPDU; } @@ -357,24 +329,17 @@ private int legitimateLegacy(string password) setVariableReq.SessionId = m_SessionId; setVariableReq.Address = Ids.ServerSessionResponse; setVariableReq.Value = new ValueUSIntArray(challengeResponse); - res = SendS7plusFunctionObject(setVariableReq); + res = SendS7plusFunctionObjectAndWait(setVariableReq, m_ReadTimeout); if (res != 0) { m_client.Disconnect(); return res; } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - m_client.Disconnect(); - return m_LastError; - } var setVariableResponse = SetVariableResponse.DeserializeFromPdu(m_ReceivedPDU); if (setVariableResponse == null) { - Console.WriteLine("S7CommPlusConnection - Legitimate: setVariableResponse with Error!"); + System.Diagnostics.Trace.WriteLine("S7CommPlusProtocolSession - Legitimate: setVariableResponse with Error!"); m_client.Disconnect(); return S7Consts.errIsoInvalidPDU; } @@ -382,7 +347,7 @@ private int legitimateLegacy(string password) Int16 errorCode = (Int16)setVariableResponse.ReturnValue; if (errorCode < 0) { - Console.WriteLine("S7CommPlusConnection - Legitimate: access denied"); + System.Diagnostics.Trace.WriteLine("S7CommPlusProtocolSession - Legitimate: access denied"); m_client.Disconnect(); return S7Consts.errCliAccessDenied; } diff --git a/src/S7CommPlusDriver/Net/Error.cs b/src/S7CommPlusDriver/Net/Error.cs new file mode 100644 index 0000000..35dc393 --- /dev/null +++ b/src/S7CommPlusDriver/Net/Error.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Linq; + +namespace S7CommPlusDriver.Net +{ + public class Error + { + private static readonly Dictionary errorsDictionary; + + static Error() + { + var fields = typeof(S7Consts).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + errorsDictionary = fields.Where(x => x.Name.StartsWith("err")).ToDictionary(x => (int)x.GetValue(null), x => x.Name); + } + + public static string GetErrorText(int error) + { + if (errorsDictionary.TryGetValue(error, out var text)) + return text; + return null; + } + } +} diff --git a/src/S7CommPlusDriver/Net/IS7TlsConnector.cs b/src/S7CommPlusDriver/Net/IS7TlsConnector.cs new file mode 100644 index 0000000..671b76c --- /dev/null +++ b/src/S7CommPlusDriver/Net/IS7TlsConnector.cs @@ -0,0 +1,19 @@ +using System; + +namespace S7CommPlusDriver +{ + internal interface IS7TlsConnector : IDisposable + { + void Write(byte[] data, int dataLength); + void ReadCompleted(byte[] data, int dataLength); + int Receive(ref byte[] buffer, int bufferSize); + byte[] GetOmsExporterSecret(); + } + + internal interface IS7TlsConnectorCallback + { + void WriteData(byte[] data, int dataLength); + void OnDataAvailable(); + void OnSslError(int sslError, string sslState); + } +} diff --git a/src/S7CommPlusDriver/Net/IS7Transport.cs b/src/S7CommPlusDriver/Net/IS7Transport.cs new file mode 100644 index 0000000..dd66f77 --- /dev/null +++ b/src/S7CommPlusDriver/Net/IS7Transport.cs @@ -0,0 +1,14 @@ +using System; + +namespace S7CommPlusDriver +{ + internal interface IS7Transport : IDisposable + { + bool Connected { get; } + int Connect(string address, int port, int connectTimeoutMilliseconds, int receiveTimeoutMilliseconds, int sendTimeoutMilliseconds); + int Send(byte[] buffer); + int Send(byte[] buffer, int size); + int Receive(byte[] buffer, int start, int size); + int Close(); + } +} diff --git a/src/S7CommPlusDriver/Net/MsgSocket.cs b/src/S7CommPlusDriver/Net/MsgSocket.cs index 297bbde..e2bf2e5 100644 --- a/src/S7CommPlusDriver/Net/MsgSocket.cs +++ b/src/S7CommPlusDriver/Net/MsgSocket.cs @@ -1,7 +1,7 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver - * + * * Based on Snap7 (Sharp7.cs) by Davide Nardella licensed under LGPL * /****************************************************************************/ @@ -9,11 +9,10 @@ using System; using System.Net.Sockets; -using System.Threading; namespace S7CommPlusDriver { - // + // class MsgSocket { private Socket TCPSocket; @@ -44,12 +43,14 @@ private void CreateSocket() { TCPSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); TCPSocket.NoDelay = true; + TCPSocket.ReceiveTimeout = _ReadTimeout; + TCPSocket.SendTimeout = _WriteTimeout; } private void TCPPing(string Host, int Port) { // To Ping the PLC an Asynchronous socket is used rather then an ICMP packet. - // This allows the use also across Internet and Firewalls (obviously the port must be opened) + // This allows the use also across Internet and Firewalls (obviously the port must be opened) LastError = 0; Socket PingSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try @@ -74,7 +75,7 @@ public int Connect(string Host, int Port) LastError = 0; if (!Connected) { - // TWI: TCPPing rausgenommen, stört bei Wireshark Analyse + // TWI: TCPPing rausgenommen, st�rt bei Wireshark Analyse //TCPPing(Host, Port); if (LastError == 0) try @@ -90,59 +91,40 @@ public int Connect(string Host, int Port) return LastError; } - private int WaitForData(int Size, int Timeout) + public int Receive(byte[] Buffer, int Start, int Size) { - bool Expired = false; - int SizeAvail; - int Elapsed = Environment.TickCount; + int bytesReadTotal = 0; LastError = 0; - try - { - SizeAvail = TCPSocket.Available; - while ((SizeAvail < Size) && (!Expired)) - { - Thread.Sleep(2); - SizeAvail = TCPSocket.Available; - Expired = Environment.TickCount - Elapsed > Timeout; - // If timeout we clean the buffer - if (Expired && (SizeAvail > 0)) - try - { - byte[] Flush = new byte[SizeAvail]; - TCPSocket.Receive(Flush, 0, SizeAvail, SocketFlags.None); - } - catch { } - } - } - catch - { - LastError = S7Consts.errTCPDataReceive; - } - if (Expired) + if (!Connected) { - LastError = S7Consts.errTCPDataReceive; + LastError = S7Consts.errTCPNotConnected; + return LastError; } - return LastError; - } - - public int Receive(byte[] Buffer, int Start, int Size) - { - - int BytesRead = 0; - LastError = WaitForData(Size, _ReadTimeout); - if (LastError == 0) + while (LastError == 0 && bytesReadTotal < Size) { try { - BytesRead = TCPSocket.Receive(Buffer, Start, Size, SocketFlags.None); + var bytesRead = TCPSocket.Receive(Buffer, Start + bytesReadTotal, Size - bytesReadTotal, SocketFlags.None); + if (bytesRead == 0) + { + LastError = S7Consts.errTCPConnectionReset; + Close(); + } + else + { + bytesReadTotal += bytesRead; + } + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut) + { + LastError = S7Consts.errTCPReceiveTimeout; } catch { LastError = S7Consts.errTCPDataReceive; } - if (BytesRead == 0) // Connection Reset by the peer + if (LastError != 0 && LastError != S7Consts.errTCPReceiveTimeout) { - LastError = S7Consts.errTCPDataReceive; Close(); } } @@ -152,10 +134,20 @@ public int Receive(byte[] Buffer, int Start, int Size) public int Send(byte[] Buffer, int Size) { LastError = 0; + if (!Connected) + { + LastError = S7Consts.errTCPNotConnected; + return LastError; + } try { int BytesSent = TCPSocket.Send(Buffer, Size, SocketFlags.None); } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut) + { + LastError = S7Consts.errTCPSendTimeout; + Close(); + } catch { LastError = S7Consts.errTCPDataSend; diff --git a/src/S7CommPlusDriver/Net/S7Client.cs b/src/S7CommPlusDriver/Net/S7Client.cs index e62afd5..856d617 100644 --- a/src/S7CommPlusDriver/Net/S7Client.cs +++ b/src/S7CommPlusDriver/Net/S7Client.cs @@ -1,13 +1,14 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver - * + * * Based on Snap7 (Sharp7.cs) by Davide Nardella licensed under LGPL * /****************************************************************************/ #endregion using OpenSsl; +using S7CommPlusDriver.Tls; using System; using System.IO; using System.Threading; @@ -19,11 +20,14 @@ namespace S7CommPlusDriver // | it under the terms of the Lesser GNU General Public License as published by | // | the Free Software Foundation, either version 3 of the License, or | // | (at your option) any later version. | - public class S7Client : OpenSSLConnector.IConnectorCallback + public class S7Client : IS7TlsConnectorCallback, IDisposable { - #region [Constants and TypeDefs] + public static bool WriteSslKeyToFile; + public static string WriteSslKeyPath; + + #region [Constants and TypeDefs] - public int _LastError = 0; + public int _LastError = 0; #endregion @@ -32,7 +36,7 @@ public class S7Client : OpenSSLConnector.IConnectorCallback // ISO Connection Request telegram (contains also ISO Header and COTP Header) byte[] ISO_CR = { // TPKT (RFC1006 Header) - 0x03, // RFC 1006 ID (3) + 0x03, // RFC 1006 ID (3) 0x00, // Reserved, always 0 0x00, // High part of packet lenght (entire frame, payload and TPDU included) 0x24, // Low part of packet lenght (entire frame, payload and TPDU included) @@ -73,21 +77,33 @@ public class S7Client : OpenSSLConnector.IConnectorCallback bool m_runThread_DoStop; IntPtr m_ptr_ssl_method; IntPtr m_ptr_ctx; - OpenSSLConnector m_sslconn; + IS7TlsConnector m_sslconn; + S7CommPlusTlsBackend m_TlsBackend = S7CommPlusTlsBackend.OpenSsl; + public string LastErrorDetail { get; private set; } = string.Empty; DateTime m_DateTimeStarted; Native.SSL_CTX_keylog_cb_func m_keylog_cb; - // OpenSSL möchte Daten auf den Socket aussenden. + // OpenSSL m�chte Daten auf den Socket aussenden. public void WriteData(byte[] pData, int dataLength) { // SSL fordert Daten zum Absenden an - // TODO: Was ist, wenn SSL Daten verschicken möchte, die größer als eine TPDU sind? - // Bei großen Zertifikaten oder ähnlichem? Fragmentierung hier? - // Console.WriteLine("S7Client - OpenSSL WriteData: dataLength=" + dataLength); - byte[] sendData = new byte[dataLength]; - Array.Copy(pData, sendData, dataLength); - SendIsoPacket(sendData); + // System.Diagnostics.Trace.WriteLine("S7Client - OpenSSL WriteData: dataLength=" + dataLength); + int offset = 0; + int maxPayloadSize = Math.Min(MaxPduSizeToRequest, PDU.Length - IsoHSize); + if (maxPayloadSize <= 0) + { + _LastError = S7Consts.errIsoInvalidPDU; + return; + } + while (offset < dataLength && _LastError == 0) + { + int chunkSize = Math.Min(maxPayloadSize, dataLength - offset); + byte[] sendData = new byte[chunkSize]; + Array.Copy(pData, offset, sendData, 0, chunkSize); + SendIsoPacket(sendData); + offset += chunkSize; + } } // OpenSSL meldet fertige Daten (decrypted) zum einlesen @@ -96,66 +112,114 @@ public void OnDataAvailable() // Netzwerk meldet eintreffende Daten byte[] buf = new byte[8192]; int bytesRead = m_sslconn.Receive(ref buf, buf.Length); - // Console.WriteLine("S7Client - OpenSSL OnDataAvailable: bytesRead=" + bytesRead); + // System.Diagnostics.Trace.WriteLine("S7Client - OpenSSL OnDataAvailable: bytesRead=" + bytesRead); byte[] readData = new byte[bytesRead]; Array.Copy(buf, readData, bytesRead); OnDataReceived?.Invoke(readData, bytesRead); } - // OpenSSL Key Callback Funktion. Gibt die ausgehandelden privaten Schlüssel aus. Kann beispielsweise - // in eine Wireshark Aufzeichnung eingefügt werden um dort die TLS Kommunikation zu entschlüsseln. + public void OnSslError(int sslError, string sslState) + { + _LastError = S7Consts.errOpenSSL; + LastErrorDetail = string.IsNullOrWhiteSpace(sslState) + ? $"OpenSSL reported SSL error {sslError}." + : $"OpenSSL reported SSL error {sslError}: {sslState}"; + OnReceiveError?.Invoke(_LastError); + } + + // OpenSSL Key Callback Funktion. Gibt die ausgehandelden privaten Schl�ssel aus. Kann beispielsweise + // in eine Wireshark Aufzeichnung eingef�gt werden um dort die TLS Kommunikation zu entschl�sseln. public void SSL_CTX_keylog_cb(IntPtr ssl, string line) { string filename = "key_" + m_DateTimeStarted.ToString("yyyyMMdd_HHmmss") + ".log"; + if (WriteSslKeyPath != null) + filename = Path.Combine(WriteSslKeyPath, filename); StreamWriter file = new StreamWriter(filename, append: true); file.WriteLine(line); file.Close(); } // Startet OpenSSL und aktiviert ab jetzt TLS - public int SslActivate() + public int SslActivate(S7CommPlusTlsBackend tlsBackend = S7CommPlusTlsBackend.OpenSsl) { int ret; + LastErrorDetail = string.Empty; try { + m_TlsBackend = tlsBackend; + if (m_TlsBackend == S7CommPlusTlsBackend.BouncyCastle) + { + var bouncyCastleConnector = new BouncyCastleTlsConnector(this); + m_sslconn = bouncyCastleConnector; + m_SslActive = true; + bouncyCastleConnector.StartHandshake(); + return 0; + } + ret = Native.OPENSSL_init_ssl(0, IntPtr.Zero); // returns 1 on success or 0 on error - if (ret != 1) - { + if (ret != 1) + { + LastErrorDetail = "OpenSSL initialization failed."; return S7Consts.errOpenSSL; } m_ptr_ssl_method = Native.ExpectNonNull(Native.TLS_client_method()); m_ptr_ctx = Native.ExpectNonNull(Native.SSL_CTX_new(m_ptr_ssl_method)); - // TLS 1.3 forcieren, da wegen TLS on IsoOnTCP bekannt sein muss, um wie viele Bytes sich die verschlüsselten - // Daten verlängern um die Pakete auf S7CommPlus-Ebene entsprechend zu fragmentieren. - // Die Verlängerung geschieht z.B. durch Padding und HMAC. Bei TLS 1.3 existiert mit GCM kein Padding und verlängert sich immer - // um 16 Bytes. Da auch TLS_CHACHA20_POLY1305_SHA256 zu den TLS 1.3 CipherSuite zählt, explizit die anderen setzen. + // TLS 1.3 forcieren, da wegen TLS on IsoOnTCP bekannt sein muss, um wie viele Bytes sich die verschl�sselten + // Daten verl�ngern um die Pakete auf S7CommPlus-Ebene entsprechend zu fragmentieren. + // Die Verl�ngerung geschieht z.B. durch Padding und HMAC. Bei TLS 1.3 existiert mit GCM kein Padding und verl�ngert sich immer + // um 16 Bytes. Da auch TLS_CHACHA20_POLY1305_SHA256 zu den TLS 1.3 CipherSuite z�hlt, explizit die anderen setzen. Native.SSL_CTX_ctrl(m_ptr_ctx, Native.SSL_CTRL_SET_MIN_PROTO_VERSION, Native.TLS1_3_VERSION, IntPtr.Zero); ret = Native.SSL_CTX_set_ciphersuites(m_ptr_ctx, "TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256"); if (ret != 1) - { + { + LastErrorDetail = "OpenSSL rejected the configured TLS 1.3 cipher suites."; return S7Consts.errOpenSSL; } - m_sslconn = new OpenSSLConnector(m_ptr_ctx, this); - m_sslconn.ExpectConnect(); + m_sslconn = new OpenSslTlsConnector(m_ptr_ctx, this); // Keylog callback setzen - m_keylog_cb = new Native.SSL_CTX_keylog_cb_func(SSL_CTX_keylog_cb); - Native.SSL_CTX_set_keylog_callback(m_ptr_ctx, m_keylog_cb); + if (WriteSslKeyToFile) + { + m_keylog_cb = new Native.SSL_CTX_keylog_cb_func(SSL_CTX_keylog_cb); + Native.SSL_CTX_set_keylog_callback(m_ptr_ctx, m_keylog_cb); + } m_SslActive = true; - } - catch - { + } + catch (Exception ex) + { + LastErrorDetail = CreateOpenSslActivationError(tlsBackend, ex); + m_SslActive = false; + m_sslconn?.Dispose(); + m_sslconn = null; return S7Consts.errOpenSSL; } return 0; } + private static string CreateOpenSslActivationError(S7CommPlusTlsBackend tlsBackend, Exception exception) + { + return tlsBackend == S7CommPlusTlsBackend.OpenSsl + ? $"OpenSSL backend could not load or initialize its native dependencies for process architecture {System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture}. {exception.GetType().Name}: {exception.Message}" + : $"TLS backend {tlsBackend} could not initialize. {exception.GetType().Name}: {exception.Message}"; + } + // Deaktiviert TLS public void SslDeactivate() { m_SslActive = false; - // TODO: Ist hier etwas zu OpenSSL-Ressourcen explizit freizugeben? + m_sslconn?.Dispose(); + m_sslconn = null; + if (m_ptr_ctx != IntPtr.Zero) + { + Native.SSL_CTX_free(m_ptr_ctx); + m_ptr_ctx = IntPtr.Zero; + } + } + + public void Dispose() + { + SslDeactivate(); } #endregion @@ -166,7 +230,7 @@ private void StartThread() m_runThread.Start(); } - // Der Task der kontinuierlich ausgeführt wird + // Der Task der kontinuierlich ausgef�hrt wird private void RunThread() { int Length; @@ -175,7 +239,6 @@ private void RunThread() // Versuchen zu lesen _LastError = 0; Length = RecvIsoPacket(); - // TODO: Hier nur den Payload zurückgeben if (Length > 0) { byte[] Buffer = new byte[Length - TPKT_ISO.Length]; Array.Copy(PDU, TPKT_ISO.Length, Buffer, 0, Length - TPKT_ISO.Length); @@ -189,11 +252,17 @@ private void RunThread() OnDataReceived?.Invoke(Buffer, Size); } } + else if (_LastError != 0 && _LastError != S7Consts.errTCPReceiveTimeout) + { + OnReceiveError?.Invoke(_LastError); + break; + } } } public _OnDataReceived OnDataReceived; public delegate void _OnDataReceived(byte[] PDU, int len); + public Action OnReceiveError; #region [Internals] @@ -219,17 +288,15 @@ private void RunThread() private byte[] RemoteTSAP_S; private byte LastPDUType; private byte[] PDU = new byte[2048]; - private MsgSocket Socket = null; + private IS7Transport Socket = null; + private readonly Func _transportFactory; private int Time_ms = 0; private void CreateSocket() { try { - Socket = new MsgSocket(); - Socket.ConnectTimeout = _ConnTimeout; - Socket.ReadTimeout = _RecvTimeout; - Socket.WriteTimeout = _SendTimeout; + Socket = _transportFactory(); } catch { @@ -241,7 +308,7 @@ private int TCPConnect() if (_LastError == 0) try { - _LastError = Socket.Connect(IPAddress, _PLCPort); + _LastError = Socket.Connect(IPAddress, _PLCPort, _ConnTimeout, _RecvTimeout, _SendTimeout); } catch { @@ -273,6 +340,7 @@ private void SendPacket(byte[] Buffer) public void Send(byte[] Buffer) { + _LastError = 0; if (m_SslActive) { m_sslconn.Write(Buffer, Buffer.Length); @@ -283,8 +351,15 @@ public void Send(byte[] Buffer) } } + internal int SendEmptyDtData() + { + byte[] emptyDtData = { 0x03, 0x00, 0x00, 0x07, 0x02, 0xF0, 0x00 }; + SendPacket(emptyDtData); + return _LastError; + } + private int SendIsoPacket(byte[] Buffer) - { + { // Packt die zu sendenden Daten in den Iso-Header ein. int Size = Buffer.Length; _LastError = 0; @@ -319,6 +394,7 @@ private int RecvIsoPacket() { Boolean Done = false; int Size = 0; + int emptyDataPacketCount = 0; while ((_LastError == 0) && !Done) { // Get TPKT (4 bytes) @@ -326,24 +402,30 @@ private int RecvIsoPacket() if (_LastError == 0) { Size = GetWordAt(PDU, 2); + if (Size < IsoHSize || Size > PDU.Length) + { + _LastError = S7Consts.errIsoInvalidPDU; + break; + } // Check 0 bytes Data Packet (only TPKT+COTP = 7 bytes) if (Size == IsoHSize) - RecvPacket(PDU, 4, 3); // Skip remaining 3 bytes and Done is still false - else { - // TODO: Größe korrekt prüfen - //if ((Size > _PduSizeRequested + IsoHSize) || (Size < MinPduSize)) - // _LastError = S7Consts.errIsoInvalidPDU; - //else - Done = true; // a valid Length !=7 && >16 && <247 + RecvPacket(PDU, 4, 3); // Skip remaining 3 bytes and Done is still false + emptyDataPacketCount++; + if (emptyDataPacketCount > 16) + { + _LastError = S7Consts.errIsoInvalidPDU; + } } + else + Done = true; } } if (_LastError == 0) { RecvPacket(PDU, 4, 3); // Skip remaining 3 COTP bytes - LastPDUType = PDU[5]; // Stores PDU Type, we need it - // Receives the S7 Payload + LastPDUType = PDU[5]; // Stores PDU Type, we need it + // Receives the S7 Payload RecvPacket(PDU, 7, Size - IsoHSize); } if (_LastError == 0) @@ -366,7 +448,7 @@ private int ISOConnect() Array.Copy(ISO_CR, isocon, 20); Array.Copy(RemoteTSAP_S, 0, isocon, 20, RemoteTSAP_S.Length); - // Sends the connection request telegram + // Sends the connection request telegram SendPacket(isocon); if (_LastError == 0) { @@ -374,13 +456,8 @@ private int ISOConnect() Size = RecvIsoPacket(); if (_LastError == 0) { - if (Size == 36) - { - if (LastPDUType != (byte)0xD0) // 0xD0 = CC Connection confirm - _LastError = S7Consts.errIsoConnect; - } - else - _LastError = S7Consts.errIsoInvalidPDU; + if (Size < IsoHSize || LastPDUType != (byte)0xD0) // 0xD0 = CC Connection confirm + _LastError = S7Consts.errIsoConnect; } } return _LastError; @@ -389,7 +466,7 @@ private int ISOConnect() public byte[] getOMSExporterSecret() { if (m_sslconn == null) return null; - return m_sslconn.getOMSExporterSecret(); + return m_sslconn.GetOmsExporterSecret(); } #endregion @@ -397,7 +474,13 @@ public byte[] getOMSExporterSecret() #region [Class Control] public S7Client() + : this(() => new SocketS7Transport()) + { + } + + internal S7Client(Func transportFactory) { + _transportFactory = transportFactory ?? throw new ArgumentNullException(nameof(transportFactory)); m_DateTimeStarted = DateTime.Now; CreateSocket(); } @@ -414,6 +497,8 @@ public int Connect() int Elapsed = Environment.TickCount; if (!Connected) { + Socket?.Close(); + CreateSocket(); TCPConnect(); // First stage : TCP Connection if (_LastError == 0) { @@ -426,7 +511,11 @@ public int Connect() } } if (_LastError != 0) + { + var connectError = _LastError; Disconnect(); + _LastError = connectError; + } else Time_ms = Environment.TickCount - Elapsed; @@ -447,13 +536,29 @@ public int SetConnectionParams(string Address, ushort LocalTSAP, byte[] RemoteTS } public int Disconnect() + { + return Disconnect(DefaultTimeout); + } + + public int Disconnect(int timeoutMilliseconds) { m_runThread_DoStop = true; - m_runThread?.Join(); + _LastError = 0; + Socket?.Close(); + if (m_runThread != null && m_runThread.IsAlive) + { + if (!m_runThread.Join(Math.Max(1, timeoutMilliseconds))) + { + _LastError = S7Consts.errCliDestroying; + } + else + { + _LastError = 0; + } + } + SslDeactivate(); - Socket.Close(); - - return 0; + return _LastError; } public int GetParam(Int32 ParamNumber, ref int Value) @@ -596,6 +701,11 @@ public static string ErrorText(int Error) case S7Consts.errCliFunctionNotImplemented: return "CLI : Function not implemented"; case S7Consts.errCliFirmwareNotSupported: return "CLI : Firmware not supported"; case S7Consts.errCliDeviceNotSupported: return "CLI : Device type not supported"; + case S7Consts.errOpenSSL: return "OPENSSL : OpenSSL error"; + case S7Consts.errInitSslResponse: return "S7COMMP : Init SSL response error"; + case S7Consts.errS7CommPlusLegacyAuthentication: return "S7COMMP : Legacy challenge authentication failed"; + case S7Consts.errS7CommPlusDigestMismatch: return "S7COMMP : Legacy packet digest mismatch"; + case S7Consts.errS7CommPlusLegacyRequestTooLarge: return "S7COMMP : Legacy request exceeds single-frame limit"; default: return "CLI : Unknown error (0x" + Convert.ToString(Error, 16) + ")"; }; } diff --git a/src/S7CommPlusDriver/Net/S7Consts.cs b/src/S7CommPlusDriver/Net/S7Consts.cs index 50488bb..0bbf15f 100644 --- a/src/S7CommPlusDriver/Net/S7Consts.cs +++ b/src/S7CommPlusDriver/Net/S7Consts.cs @@ -34,7 +34,19 @@ public static class S7Consts public const int errTCPUnreachableHost = 0x00002751; public const int errIsoConnect = 0x00010000; // Connection error - public const int errIsoInvalidPDU = 0x00030000; // Bad format + public const int errIsoInvalidPDU = 0x00030000; // Bad format + public const int errIsoInvalidPDU1 = 0x00030001; // Bad format + public const int errIsoInvalidPDU2 = 0x00030002; // Bad format + public const int errIsoInvalidPDU3 = 0x00030003; // Bad format + public const int errIsoInvalidPDU4 = 0x00030004; // Bad format + public const int errIsoInvalidPDU5 = 0x00030005; // Bad format + public const int errIsoInvalidPDU6 = 0x00030006; // Bad format + public const int errIsoInvalidPDU7 = 0x00030007; // Bad format + public const int errIsoInvalidPDU8 = 0x00030008; // Bad format + public const int errIsoInvalidPDU9 = 0x00030009; // Bad format + public const int errIsoInvalidPDU10 = 0x00030010; // Bad format + public const int errIsoInvalidPDU11 = 0x00030011; // Bad format + public const int errIsoInvalidPDU12 = 0x00030012; // Bad format public const int errIsoInvalidDataSize = 0x00040000; // Bad Datasize passed to send/recv : buffer is invalid public const int errCliNegotiatingPDU = 0x00100000; @@ -78,9 +90,13 @@ public static class S7Consts public const int errCliDeviceNotSupported = 0x02900000; public const int errOpenSSL = 0x03100000; + public const int errInitSslResponse = 0x03100001; + public const int errS7CommPlusLegacyAuthentication = 0x03100002; + public const int errS7CommPlusDigestMismatch = 0x03100003; + public const int errS7CommPlusLegacyRequestTooLarge = 0x03100004; //------------------------------------------------------------------------------ - // PARAMS LIST FOR COMPATIBILITY WITH Snap7.net.cs - //------------------------------------------------------------------------------ + // PARAMS LIST FOR COMPATIBILITY WITH Snap7.net.cs + //------------------------------------------------------------------------------ public const Int32 p_u16_LocalPort = 1; // Not applicable here public const Int32 p_u16_RemotePort = 2; public const Int32 p_i32_PingTimeout = 3; diff --git a/src/S7CommPlusDriver/Net/SocketS7Transport.cs b/src/S7CommPlusDriver/Net/SocketS7Transport.cs new file mode 100644 index 0000000..b8ac064 --- /dev/null +++ b/src/S7CommPlusDriver/Net/SocketS7Transport.cs @@ -0,0 +1,47 @@ +namespace S7CommPlusDriver +{ + internal sealed class SocketS7Transport : IS7Transport + { + private MsgSocket _socket; + + public bool Connected => _socket?.Connected == true; + + public int Connect(string address, int port, int connectTimeoutMilliseconds, int receiveTimeoutMilliseconds, int sendTimeoutMilliseconds) + { + _socket = new MsgSocket + { + ConnectTimeout = connectTimeoutMilliseconds, + ReadTimeout = receiveTimeoutMilliseconds, + WriteTimeout = sendTimeoutMilliseconds + }; + return _socket.Connect(address, port); + } + + public int Send(byte[] buffer) + { + return _socket?.Send(buffer, buffer.Length) ?? S7Consts.errTCPNotConnected; + } + + public int Send(byte[] buffer, int size) + { + return _socket?.Send(buffer, size) ?? S7Consts.errTCPNotConnected; + } + + public int Receive(byte[] buffer, int start, int size) + { + return _socket?.Receive(buffer, start, size) ?? S7Consts.errTCPNotConnected; + } + + public int Close() + { + _socket?.Close(); + _socket = null; + return 0; + } + + public void Dispose() + { + Close(); + } + } +} diff --git a/src/S7CommPlusDriver/OpenSSL-dll-x64/libcrypto-3-x64.dll b/src/S7CommPlusDriver/OpenSSL-dll-x64/libcrypto-3-x64.dll deleted file mode 100644 index 87c2e0a..0000000 Binary files a/src/S7CommPlusDriver/OpenSSL-dll-x64/libcrypto-3-x64.dll and /dev/null differ diff --git a/src/S7CommPlusDriver/OpenSSL-dll-x64/libssl-3-x64.dll b/src/S7CommPlusDriver/OpenSSL-dll-x64/libssl-3-x64.dll deleted file mode 100644 index 64f9712..0000000 Binary files a/src/S7CommPlusDriver/OpenSSL-dll-x64/libssl-3-x64.dll and /dev/null differ diff --git a/src/S7CommPlusDriver/OpenSSL-dll-x86/libcrypto-3.dll b/src/S7CommPlusDriver/OpenSSL-dll-x86/libcrypto-3.dll deleted file mode 100644 index 40192cf..0000000 Binary files a/src/S7CommPlusDriver/OpenSSL-dll-x86/libcrypto-3.dll and /dev/null differ diff --git a/src/S7CommPlusDriver/OpenSSL-dll-x86/libssl-3.dll b/src/S7CommPlusDriver/OpenSSL-dll-x86/libssl-3.dll deleted file mode 100644 index 5757185..0000000 Binary files a/src/S7CommPlusDriver/OpenSSL-dll-x86/libssl-3.dll and /dev/null differ diff --git a/src/S7CommPlusDriver/OpenSSL/Native.cs b/src/S7CommPlusDriver/OpenSSL/Native.cs index 1647842..a9f383c 100644 --- a/src/S7CommPlusDriver/OpenSSL/Native.cs +++ b/src/S7CommPlusDriver/OpenSSL/Native.cs @@ -14,26 +14,82 @@ #endregion using System; +using System.IO; +using System.Reflection; using System.Runtime.InteropServices; using System.Text; namespace OpenSsl { -#if _WIN64 - using size_t = UInt64; -#else - using size_t = UInt32; -#endif + public class Native - { -#if _WIN64 - const string DLLNAME = "libcrypto-3-x64.dll"; - const string SSLDLLNAME = "libssl-3-x64.dll"; -#else - const string DLLNAME = "libcrypto-3.dll"; - const string SSLDLLNAME = "libssl-3.dll"; -#endif + { + const string DLLNAME = "libcrypto-3"; + const string SSLDLLNAME = "libssl-3"; + static Native() + { + NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), DllImportResolver); + } + + private static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + { + if (libraryName == DLLNAME) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && RuntimeInformation.ProcessArchitecture == Architecture.X86) + return LoadRuntimeNativeLibrary(assembly, searchPath, "win-x86", "libcrypto-3.dll"); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && RuntimeInformation.ProcessArchitecture == Architecture.X64) + return LoadRuntimeNativeLibrary(assembly, searchPath, "win-x64", "libcrypto-3-x64.dll"); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) + return LoadRuntimeNativeLibrary(assembly, searchPath, "win-arm64", "libcrypto-3-arm64.dll"); + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) + return LoadRuntimeNativeLibrary(assembly, searchPath, "osx-arm64", "libcrypto.3.dylib"); + return IntPtr.Zero; + } + + if(libraryName == SSLDLLNAME) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && RuntimeInformation.ProcessArchitecture == Architecture.X86) + return LoadRuntimeNativeLibrary(assembly, searchPath, "win-x86", "libssl-3.dll"); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && RuntimeInformation.ProcessArchitecture == Architecture.X64) + return LoadRuntimeNativeLibrary(assembly, searchPath, "win-x64", "libssl-3-x64.dll"); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) + return LoadRuntimeNativeLibrary(assembly, searchPath, "win-arm64", "libssl-3-arm64.dll"); + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && RuntimeInformation.ProcessArchitecture == Architecture.Arm64) + return LoadRuntimeNativeLibrary(assembly, searchPath, "osx-arm64", "libssl.3.dylib"); + return IntPtr.Zero; + } + + // Otherwise, fallback to default import resolver. + return IntPtr.Zero; + } + + private static IntPtr LoadRuntimeNativeLibrary(Assembly assembly, DllImportSearchPath? searchPath, string runtimeIdentifier, string fileName) + { + var path = RuntimeNativePath(assembly, runtimeIdentifier, fileName); + try + { + return NativeLibrary.Load(path, assembly, searchPath); + } + catch (Exception ex) when (ex is DllNotFoundException || ex is BadImageFormatException) + { + throw new DllNotFoundException( + $"Could not load native OpenSSL dependency '{path}' for process architecture {RuntimeInformation.ProcessArchitecture}. " + + "Install the matching Visual C++ runtime or package dependent runtime DLLs app-local beside the OpenSSL binaries.", + ex); + } + } + + private static string RuntimeNativePath(Assembly assembly, string runtimeIdentifier, string fileName) + { + var assemblyDirectory = Path.GetDirectoryName(assembly.Location); + if (string.IsNullOrEmpty(assemblyDirectory)) + { + return Path.Combine("runtimes", runtimeIdentifier, "native", fileName); + } + + return Path.Combine(assemblyDirectory, "runtimes", runtimeIdentifier, "native", fileName); + } #region Delegates @@ -227,7 +283,7 @@ public class Native public static extern IntPtr TLS_client_method(); [DllImport(SSLDLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr SSL_export_keying_material(IntPtr ssl, byte[] outKeyMaterial, size_t outKeyMaterialLength, char[] label, size_t labelLength, IntPtr context, size_t contextLength, int useContext); + public static extern IntPtr SSL_export_keying_material(IntPtr ssl, byte[] outKeyMaterial, nint outKeyMaterialLength, char[] label, nint labelLength, IntPtr context, nint contextLength, int useContext); #endregion @@ -264,7 +320,7 @@ public class Native // size_t BIO_ctrl_pending(BIO *b); [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] - public static extern size_t BIO_ctrl_pending(IntPtr bio); + public static extern nint BIO_ctrl_pending(IntPtr bio); // void BIO_free_all(BIO *a); [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] @@ -323,7 +379,7 @@ public static long BIO_set_mem_eof_return(IntPtr b, int v) // void ERR_error_string_n(unsigned long e, char *buf, size_t len); [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] //public static extern void ERR_error_string_n(ulong e, IntPtr buf, size_t len); - public static extern void ERR_error_string_n(ulong e, byte[] buf, size_t len); + public static extern void ERR_error_string_n(ulong e, byte[] buf, nint len); // unsigned long ERR_get_error(void); [DllImport(DLLNAME, CallingConvention = CallingConvention.Cdecl)] diff --git a/src/S7CommPlusDriver/OpenSSL/OpenSSLConnector.cs b/src/S7CommPlusDriver/OpenSSL/OpenSSLConnector.cs index c621157..f94738a 100644 --- a/src/S7CommPlusDriver/OpenSSL/OpenSSLConnector.cs +++ b/src/S7CommPlusDriver/OpenSSL/OpenSSLConnector.cs @@ -16,11 +16,12 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Runtime.InteropServices; using DataBufferList = System.Collections.Generic.LinkedList; namespace OpenSsl { - public class OpenSSLConnector + public class OpenSSLConnector : IDisposable { private bool m_readRequired; private readonly IntPtr m_pSslConnection; // SSL* @@ -32,11 +33,14 @@ public class OpenSSLConnector private readonly byte[] m_buffer = new byte[4096]; private readonly DataBufferList m_pendingWriteList; private readonly DataBufferList m_pendingReadList; + private bool m_fatalError; + private bool m_disposed; public interface IConnectorCallback { void WriteData(byte[] pData, int dataLength); void OnDataAvailable(); + void OnSslError(int sslError, string sslState); } public class DataBuffer @@ -53,7 +57,7 @@ public DataBuffer(byte[] dat, int datLength) public void ConsumeAndRemove(int bytesUsed) { - Buffer.BlockCopy(data, bytesUsed, data, 0, bytesUsed); + Buffer.BlockCopy(data, bytesUsed, data, 0, used - bytesUsed); used -= bytesUsed; } }; @@ -74,7 +78,24 @@ public OpenSSLConnector(IntPtr pContext, IConnectorCallback dataSink) ~OpenSSLConnector() { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (m_disposed) + { + return; + } + Native.SSL_free(m_pSslConnection); + m_disposed = true; } private int DataToWrite(byte[] pData, int dataLength) @@ -83,7 +104,7 @@ private int DataToWrite(byte[] pData, int dataLength) int result = Native.SSL_write(m_pSslConnection, pData, dataLength); - if (result < 0) + if (result <= 0) { HandleError(result); } @@ -105,6 +126,11 @@ private int DataToRead(byte[] pData, int dataLength) m_readRequired = false; int bytesUsed = Native.BIO_write(m_pBioIn, pData, dataLength); + if (bytesUsed <= 0) + { + ReportFatalError(Native.SSL_ERROR_SYSCALL); + return 0; + } byte[] pBuffer = null; int bufferSize = 0; @@ -122,9 +148,13 @@ private int DataToRead(byte[] pData, int dataLength) OnDataToRead(pBuffer, bytesOut); } - if (bytesOut < 0) + if (bytesOut <= 0) { HandleError(bytesOut); + if (m_fatalError) + { + break; + } } } while (bytesOut > 0); @@ -134,7 +164,7 @@ private int DataToRead(byte[] pData, int dataLength) private void SendPendingData() { - while (Native.BIO_ctrl_pending(m_pBioOut) > 0) + while (!m_fatalError && Native.BIO_ctrl_pending(m_pBioOut) > 0) { byte[] pBuffer = null; int bufferSize = 0; @@ -154,6 +184,10 @@ private void SendPendingData() { HandleError(bytesToSend); } + else + { + ReportFatalError(Native.SSL_ERROR_SYSCALL); + } } } } @@ -171,29 +205,74 @@ void HandleError(int result) switch (error) { - case Native.SSL_ERROR_ZERO_RETURN: case Native.SSL_ERROR_NONE: case Native.SSL_ERROR_WANT_READ: + case Native.SSL_ERROR_WANT_WRITE: + case Native.SSL_ERROR_WANT_CONNECT: + case Native.SSL_ERROR_WANT_ACCEPT: // States that can occur in a normal state break; + case Native.SSL_ERROR_ZERO_RETURN: + ReportFatalError(error); + break; default: - // TOOO: Handle all other errors which don't should occur. - // 5 with SSL_ASYNC_PAUSED has been seen... - Trace.WriteLine("OpenSSL HandleError: Error = " + error); + ReportFatalError(error); break; } } } + private void ReportFatalError(int error) + { + if (m_fatalError) + { + return; + } + + m_fatalError = true; + var state = Marshal.PtrToStringAnsi(Native.SSL_state_string_long(m_pSslConnection)) ?? string.Empty; + var errorDetail = DrainErrorQueue(); + if (!string.IsNullOrEmpty(errorDetail)) + { + state = string.IsNullOrEmpty(state) ? errorDetail : state + "; " + errorDetail; + } + Trace.WriteLine("OpenSSL fatal error: Error = " + error + " State = " + state); + m_DataSink.OnSslError(error, state); + } + + private static string DrainErrorQueue() + { + var messages = new List(); + ulong error; + while ((error = Native.ERR_get_error()) != 0) + { + var buffer = new byte[256]; + Native.ERR_error_string_n(error, buffer, buffer.Length); + var message = Marshal.PtrToStringAnsi(Marshal.UnsafeAddrOfPinnedArrayElement(buffer, 0)); + if (!string.IsNullOrWhiteSpace(message)) + { + messages.Add(message); + } + } + + return messages.Count == 0 ? string.Empty : string.Join("; ", messages); + } + protected void RunSSL() { bool dataToWrite = false; bool dataToRead = false; GetPendingOperations(ref dataToRead, ref dataToWrite); + int noProgressCount = 0; - while ((!m_readRequired && dataToWrite) || dataToRead) + while (!m_fatalError && ((!m_readRequired && dataToWrite) || dataToRead)) { + int pendingReadBefore = m_pendingReadList.Count; + int pendingWriteBefore = m_pendingWriteList.Count; + bool readRequiredBefore = m_readRequired; + var bioPendingBefore = Native.BIO_ctrl_pending(m_pBioOut); + if (Native.SSL_in_init(m_pSslConnection) != 0) { // Client waiting in connect @@ -215,6 +294,23 @@ protected void RunSSL() } GetPendingOperations(ref dataToRead, ref dataToWrite); + var bioPendingAfter = Native.BIO_ctrl_pending(m_pBioOut); + if (pendingReadBefore == m_pendingReadList.Count && + pendingWriteBefore == m_pendingWriteList.Count && + readRequiredBefore == m_readRequired && + bioPendingBefore == bioPendingAfter) + { + noProgressCount++; + if (noProgressCount > 1) + { + ReportFatalError(Native.SSL_ERROR_SYSCALL); + break; + } + } + else + { + noProgressCount = 0; + } } } @@ -276,7 +372,12 @@ private void OnDataToRead(byte[] pData, int dataLength) while (m_bytesAvailable > 0) { m_DataSink.OnDataAvailable(); - // Sink sollte anschließend Receive aufrufen um Daten abzuholen + if (m_bytesAvailable == dataLength) + { + ReportFatalError(Native.SSL_ERROR_SYSCALL); + break; + } + dataLength = m_bytesAvailable; } } @@ -353,7 +454,7 @@ void PutbackBuffer(DataBufferList list, DataBuffer pBuffer) public byte[] getOMSExporterSecret() { byte[] secretOut = new byte[32]; - int ret = (int)Native.SSL_export_keying_material(m_pSslConnection, secretOut, (uint)secretOut.Length, "EXPERIMENTAL_OMS".ToCharArray(), 16, IntPtr.Zero, 0, 0); + int ret = (int)Native.SSL_export_keying_material(m_pSslConnection, secretOut, (nint)secretOut.Length, "EXPERIMENTAL_OMS".ToCharArray(), 16, IntPtr.Zero, 0, 0); return secretOut; } } diff --git a/src/S7CommPlusDriver/OpenSSL/OpenSslTlsConnector.cs b/src/S7CommPlusDriver/OpenSSL/OpenSslTlsConnector.cs new file mode 100644 index 0000000..6574ba6 --- /dev/null +++ b/src/S7CommPlusDriver/OpenSSL/OpenSslTlsConnector.cs @@ -0,0 +1,65 @@ +using System; + +namespace OpenSsl +{ + internal sealed class OpenSslTlsConnector : S7CommPlusDriver.IS7TlsConnector + { + private readonly OpenSSLConnector _connector; + + public OpenSslTlsConnector(IntPtr context, S7CommPlusDriver.IS7TlsConnectorCallback dataSink) + { + _connector = new OpenSSLConnector(context, new CallbackAdapter(dataSink)); + _connector.ExpectConnect(); + } + + public void Write(byte[] data, int dataLength) + { + _connector.Write(data, dataLength); + } + + public void ReadCompleted(byte[] data, int dataLength) + { + _connector.ReadCompleted(data, dataLength); + } + + public int Receive(ref byte[] buffer, int bufferSize) + { + return _connector.Receive(ref buffer, bufferSize); + } + + public byte[] GetOmsExporterSecret() + { + return _connector.getOMSExporterSecret(); + } + + public void Dispose() + { + _connector.Dispose(); + } + + private sealed class CallbackAdapter : OpenSSLConnector.IConnectorCallback + { + private readonly S7CommPlusDriver.IS7TlsConnectorCallback _dataSink; + + public CallbackAdapter(S7CommPlusDriver.IS7TlsConnectorCallback dataSink) + { + _dataSink = dataSink; + } + + public void WriteData(byte[] pData, int dataLength) + { + _dataSink.WriteData(pData, dataLength); + } + + public void OnDataAvailable() + { + _dataSink.OnDataAvailable(); + } + + public void OnSslError(int sslError, string sslState) + { + _dataSink.OnSslError(sslError, sslState); + } + } + } +} diff --git a/src/S7CommPlusDriver/Properties/AssemblyInfo.cs b/src/S7CommPlusDriver/Properties/AssemblyInfo.cs index ef803d9..9c30261 100644 --- a/src/S7CommPlusDriver/Properties/AssemblyInfo.cs +++ b/src/S7CommPlusDriver/Properties/AssemblyInfo.cs @@ -1,36 +1,5 @@ -using System.Reflection; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -// Allgemeine Informationen über eine Assembly werden über die folgenden -// Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, -// die einer Assembly zugeordnet sind. -[assembly: AssemblyTitle("S7CommPlusDriver")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("S7CommPlusDriver")] -[assembly: AssemblyCopyright("Copyright © 2023, Thomas Wiens")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Durch Festlegen von ComVisible auf FALSE werden die Typen in dieser Assembly -// für COM-Komponenten unsichtbar. Wenn Sie auf einen Typ in dieser Assembly von -// COM aus zugreifen müssen, sollten Sie das ComVisible-Attribut für diesen Typ auf "True" festlegen. -[assembly: ComVisible(false)] - -// Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird -[assembly: Guid("781951d8-80de-4e2f-92ca-a5fef0059415")] - -// Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten: -// -// Hauptversion -// Nebenversion -// Buildnummer -// Revision -// -// Sie können alle Werte angeben oder Standardwerte für die Build- und Revisionsnummern verwenden, -// indem Sie "*" wie unten gezeigt eingeben: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.1.0.0")] -[assembly: AssemblyFileVersion("0.1.0.0")] +[assembly: InternalsVisibleTo("S7CommPlusDriver.Tests")] +[assembly: InternalsVisibleTo("DriverTest")] +[assembly: InternalsVisibleTo("S7CommPlusGUIBrowser")] diff --git a/src/S7CommPlusDriver/README.md b/src/S7CommPlusDriver/README.md new file mode 100644 index 0000000..c91d57f --- /dev/null +++ b/src/S7CommPlusDriver/README.md @@ -0,0 +1,81 @@ +# DotNetProjects.S7CommPlusDriver + +Production-oriented S7CommPlus communication library for Siemens S7-1200/1500 PLCs. + +Install from [NuGet](https://www.nuget.org/packages/DotNetProjects.S7CommPlusDriver): + +```powershell +dotnet add package DotNetProjects.S7CommPlusDriver +``` + +Use `S7CommPlusClient` for new applications. It provides async connect, browse, read, write, alarm, subscription, CPU metadata/control, block metadata, online block-view, and legitimation operations with request serialization, typed exceptions, connection-state events, reconnect support for read operations, and an explicit write-enable safety gate. + +TLS is the default security mode, using the managed BouncyCastle TLS backend unless `S7CommPlusClientOptions.TlsBackend` is set explicitly. On `net8.0` and later, older S7-1200/1500 CPUs can use `S7CommPlusSecurityMode.LegacyChallenge` or `Auto` for HarpoS7-derived legacy challenge authentication. `net6.0` remains TLS-only. + +Default connection parameters are exposed through `S7CommPlusDefaults`: ISO-on-TCP port `102`, local TSAP `0x0600`, HMI remote TSAP `SIMATIC-ROOT-HMI`, and engineering remote TSAP `SIMATIC-ROOT-ES`. Remote TSAP values are validated as ASCII COTP parameters before connecting. + +## Older PLCs / Legacy Challenge Auth + +Siemens OMS names the old non-TLS mode `SecurityTypeCSI`. This library exposes it as `S7CommPlusSecurityMode.LegacyChallenge` and keeps TLS as the default, so there is no silent security downgrade. + +```csharp +await using var client = new S7CommPlusClient(new S7CommPlusClientOptions +{ + Address = "10.0.98.34", + SecurityMode = S7CommPlusSecurityMode.LegacyChallenge, + WriteEnabled = false +}); + +await client.ConnectAsync(); +var cpuInfo = await client.GetCpuInfoAsync(); +await client.DisconnectAsync(); +``` + +Legacy support uses the PLC fingerprint to resolve a Siemens public-key family. Known mappings are S7-1500 (`00`), S7-1200 (`01`), and PLCSIM/VPLC (`03`). The implementation references the `HarpoS7` and `HarpoS7.PublicKeys` NuGet packages for challenge/key/digest primitives on `net8.0` and `net9.0`, while this driver still owns transport, request ordering, timeouts, reconnect behavior, and write protection. + +## Subscriptions + +Variable and alarm subscriptions are available through disposable production objects: + +```csharp +var tag = await client.GetTagBySymbolAsync("MyDb.MyValue"); +await using var subscription = await client.SubscribeTagsAsync(new[] { tag }); + +subscription.NotificationReceived += (_, e) => +{ + foreach (var item in e.Notification.Items) + { + if (item.IsSuccess) + { + Console.WriteLine(item.Tag); + } + } +}; +``` + +Subscriptions share the client connection with normal request/response calls. The driver serializes foreground requests on that physical connection and routes subscription notifications by PLC subscription object id, so reads and metadata requests can run while subscriptions are active without creating another PLC connection. Alarm subscriptions use `SubscribeAlarmsAsync(languageId)` and expose `NotificationReceived`, `CommunicationError`, `StateChanged`, and `Completion` in the same way. Use `SubscribeAlarmsAsync()` or `GetActiveAlarmsAsync()` without a language id to request every alarm text language returned by the PLC; the legacy `AlarmTexts` property contains the first returned language, and `AlarmTextsByLanguage` contains the full set. The library does not silently open a second PLC connection for alarm snapshots. If you need a live alarm subscription plus an initial active-alarm snapshot on a separate physical connection, create a second `S7CommPlusClient` yourself and pass it to the `SubscribeAlarmsWithSnapshotAsync(snapshotClient, ...)` overload. + +## PLC Text Lists for Alarms + +Alarm text payloads can contain TIA-style text-list placeholders such as `@2%t#519K@`. Load the PLC text-list catalog and pass it to the alarm APIs to resolve those placeholders, including nested system-diagnostic text-list references: + +```csharp +var textLists = await client.GetTextListsAsync(); // all CPU LCIDs + language-independent system lists +var alarms = await client.GetActiveAlarmsAsync(1031, textLists); + +await using var alarmSubscription = await client.SubscribeAlarmsAsync(1031, textLists); +``` + +Use `GetTextListsAsync(new[] { 1031, 2057 })` when only selected LCIDs are needed. The catalog remains one API, but each `S7CommPlusTextList` has `TextListType` (`User`, `System`, or `Unknown`) so applications can filter or display the same distinction shown by engineering tools. The online PLC payload does not carry the exact engineering project object kind, so classification follows Siemens runtime list-id conventions; placeholder resolution still uses both user and system lists. + +Associated-value placeholders are formatted too. `@2W%d@` means the second associated value, read as a `WORD`, displayed as signed decimal. + +Each `S7CommPlusAlarm` exposes `SourceRelationId` and `SourceAlarmId`, decoded from `CpuAlarmId`. For PLC program alarms, `SourceRelationId` can be matched with a separately built online block/catalog model to find the source block. + +Communication limits are exposed through `GetCommunicationResourcesAsync()`, including max read/write batch sizes, available PLC subscription slots, and subscription memory. + +## Block Metadata and Online View + +Block metadata is available through `BrowseBlocksAsync()`, `GetPlcStructureXmlAsync()`, `BrowseBlockStructureAsync()`, and `GetBlockContentAsync(relid)`. + +Advanced block-watch scenarios can use `OpenBlockOnlineViewAsync()` with a caller-provided `S7CommPlusTisWatchRequest`; the returned `S7CommPlusTisWatchSubscription` exposes parsed watch notifications and follows the same disposable subscription lifecycle. diff --git a/src/S7CommPlusDriver/S7CommPlusClient.cs b/src/S7CommPlusDriver/S7CommPlusClient.cs new file mode 100644 index 0000000..54dfd14 --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusClient.cs @@ -0,0 +1,1178 @@ +using Microsoft.Extensions.Logging; +using S7CommPlusDriver.Alarming; +using S7CommPlusDriver.ClientApi; +using S7CommPlusDriver.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace S7CommPlusDriver +{ + public sealed class S7CommPlusClient : IAsyncDisposable + { + private readonly S7CommPlusClientOptions _options; + private readonly Func _sessionFactory; + private readonly SemaphoreSlim _operationGate = new SemaphoreSlim(1, 1); + private IS7CommPlusSession _session; + private bool _disposed; + private S7CommPlusConnectionState _state = S7CommPlusConnectionState.Disconnected; + private const int CpuStopRequest = 1; + private const int CpuRunRequest = 3; + + public S7CommPlusClient(S7CommPlusClientOptions options) + : this(options, () => new S7CommPlusProtocolSession()) + { + } + + internal S7CommPlusClient(S7CommPlusClientOptions options, Func sessionFactory) + { + _options = options?.Clone() ?? throw new ArgumentNullException(nameof(options)); + _options.Validate(); + _sessionFactory = sessionFactory ?? throw new ArgumentNullException(nameof(sessionFactory)); + } + + public event EventHandler ConnectionStateChanged; + public event EventHandler CommunicationError; + + public S7CommPlusConnectionState State => _state; + public bool IsConnected => _session?.IsConnected == true && _state == S7CommPlusConnectionState.Connected; + public S7CommPlusClientOptions Options => _options.Clone(); + + public async Task ConnectAsync(CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + await _operationGate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + await ConnectCoreAsync(S7CommPlusConnectionState.Connecting, cancellationToken).ConfigureAwait(false); + } + finally + { + _operationGate.Release(); + } + } + + public async Task DisconnectAsync(CancellationToken cancellationToken = default) + { + if (_disposed) + { + return; + } + + await _operationGate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + await DisconnectCoreAsync(cancellationToken).ConfigureAwait(false); + } + finally + { + _operationGate.Release(); + } + } + + public Task> BrowseAsync(CancellationToken cancellationToken = default) + { + return ExecuteReadOperationAsync("Browse", session => + { + var error = session.BrowseVariables(out var vars); + ThrowIfError("Browse", error); + return (IReadOnlyList)(vars ?? new List()); + }, cancellationToken); + } + + public Task> BrowseBlocksAsync(CancellationToken cancellationToken = default) + { + return ExecuteReadOperationAsync("BrowseBlocks", session => + { + var error = session.BrowseBlocks(out var blocks); + ThrowIfError("BrowseBlocks", error); + return (IReadOnlyList)(blocks ?? new List()); + }, cancellationToken); + } + + public Task GetPlcStructureXmlAsync(CancellationToken cancellationToken = default) + { + return ExecuteReadOperationAsync("GetPlcStructureXml", session => + { + var error = session.GetPlcStructureXml(out var structure); + ThrowIfError("GetPlcStructureXml", error); + return structure ?? PlcStructureXmlParser.CreateSnapshot(string.Empty); + }, cancellationToken); + } + + public Task> BrowseBlockStructureAsync(CancellationToken cancellationToken = default) + { + return ExecuteReadOperationAsync("BrowseBlockStructure", session => + { + var error = session.GetPlcStructureXml(out var structure); + ThrowIfError("BrowseBlockStructure", error); + try + { + return structure?.Structure ?? Array.Empty(); + } + catch (Exception ex) + { + throw new S7CommPlusConnectionException( + "BrowseBlockStructure", + Endpoint, + S7Consts.errIsoInvalidPDU, + false, + $"BrowseBlockStructure failed for PLC {Endpoint}: PLC structure XML could not be parsed.", + ex); + } + }, cancellationToken); + } + + public Task GetBlockContentAsync(uint relid, CancellationToken cancellationToken = default) + { + return ExecuteReadOperationAsync("GetBlockContent", session => + { + var error = session.GetBlockContent(relid, out var blockContent); + ThrowIfError("GetBlockContent", error); + return blockContent; + }, cancellationToken); + } + + public Task GetCpuInfoAsync(CancellationToken cancellationToken = default) + { + return ExecuteReadOperationAsync("GetCpuInfo", session => + { + var error = session.GetCpuInfo(out var cpuInfo); + ThrowIfError("GetCpuInfo", error); + return cpuInfo; + }, cancellationToken); + } + + public Task GetCpuStateAsync(CancellationToken cancellationToken = default) + { + return ExecuteReadOperationAsync("GetCpuState", session => + { + var error = session.GetCpuState(out var cpuState); + ThrowIfError("GetCpuState", error); + return cpuState; + }, cancellationToken); + } + + public Task GetCpuCycleTimeAsync(CancellationToken cancellationToken = default) + { + return ExecuteReadOperationAsync("GetCpuCycleTime", session => + { + var error = session.GetCpuCycleTime(out var cycleTime); + ThrowIfError("GetCpuCycleTime", error); + return cycleTime; + }, cancellationToken); + } + + public Task GetCpuMemoryUsageAsync(CancellationToken cancellationToken = default) + { + return ExecuteReadOperationAsync("GetCpuMemoryUsage", session => + { + var error = session.GetCpuMemoryUsage(out var memoryUsage); + ThrowIfError("GetCpuMemoryUsage", error); + return memoryUsage; + }, cancellationToken); + } + + public Task StopCpuAsync(CancellationToken cancellationToken = default) + { + return SetCpuOperatingStateAsync("StopCpu", CpuStopRequest, cancellationToken); + } + + public Task StartCpuAsync(CancellationToken cancellationToken = default) + { + return SetCpuOperatingStateAsync("StartCpu", CpuRunRequest, cancellationToken); + } + + public Task GetCpuCultureInfoAsync(CancellationToken cancellationToken = default) + { + return ExecuteReadOperationAsync("GetCpuCultureInfo", session => + { + var error = session.GetCpuCultureInfo(out var cultureInfo); + ThrowIfError("GetCpuCultureInfo", error); + return cultureInfo; + }, cancellationToken); + } + + /// + /// Reads PLC text lists for all CPU languages, plus language-independent system text lists. + /// + public Task GetTextListsAsync(CancellationToken cancellationToken = default) + { + return GetTextListsAsync(Array.Empty(), cancellationToken); + } + + /// + /// Reads PLC text lists for the requested LCIDs, plus language-independent system text lists. + /// Pass an empty collection to request all CPU languages. + /// + public Task GetTextListsAsync(IEnumerable languageIds, CancellationToken cancellationToken = default) + { + var languageIdList = languageIds?.ToList() ?? new List(); + if (languageIdList.Any(languageId => languageId < 0 || languageId > UInt16.MaxValue)) + { + throw new ArgumentOutOfRangeException(nameof(languageIds), "Language ids must be positive LCID values."); + } + + return ExecuteReadOperationAsync("GetTextLists", session => + { + var error = session.GetTextLists(languageIdList, out var textLists); + ThrowIfError("GetTextLists", error); + return textLists ?? S7CommPlusTextListCatalog.Empty; + }, cancellationToken); + } + + public Task GetCommunicationResourcesAsync(CancellationToken cancellationToken = default) + { + return ExecuteReadOperationAsync("GetCommunicationResources", session => + { + var error = session.GetCommunicationResources(out var resources); + ThrowIfError("GetCommunicationResources", error); + return new S7CommPlusCommunicationResources(resources); + }, cancellationToken); + } + + public async Task LegitimateAsync(string password, string username = "", CancellationToken cancellationToken = default) + { + if (password == null) + { + throw new ArgumentNullException(nameof(password)); + } + username ??= string.Empty; + + await ExecuteSessionOperationAsync("Legitimate", session => + { + var error = session.Legitimate(password, username); + ThrowIfError("Legitimate", error); + return true; + }, cancellationToken).ConfigureAwait(false); + } + + /// + /// Reads the currently active PLC alarms and requests all alarm text languages returned by the PLC. + /// The legacy property contains the first returned language; + /// use to access every returned language. + /// + public Task> GetActiveAlarmsAsync(CancellationToken cancellationToken = default) + { + return GetActiveAlarmsCoreAsync(0, cancellationToken); + } + + /// + /// Reads the currently active PLC alarms and selects the requested alarm text language from the returned text payload. + /// + /// LCID to expose through , for example 1031 for de-DE. + public Task> GetActiveAlarmsAsync(int languageId, CancellationToken cancellationToken = default) + { + return GetActiveAlarmsAsync(languageId, null, cancellationToken); + } + + /// + /// Reads active PLC alarms and resolves text-list placeholders using a catalog returned by . + /// + public Task> GetActiveAlarmsAsync(S7CommPlusTextListCatalog textLists, CancellationToken cancellationToken = default) + { + return GetActiveAlarmsCoreAsync(0, CreateTextListResolver(textLists), cancellationToken); + } + + /// + /// Reads active PLC alarms for one LCID and resolves text-list placeholders using a catalog returned by . + /// + public Task> GetActiveAlarmsAsync(int languageId, S7CommPlusTextListCatalog textLists, CancellationToken cancellationToken = default) + { + if (languageId < 0) + { + throw new ArgumentOutOfRangeException(nameof(languageId), "Language ids must be positive LCID values."); + } + + return GetActiveAlarmsCoreAsync(languageId, CreateTextListResolver(textLists), cancellationToken); + } + + private Task> GetActiveAlarmsCoreAsync(int alarmTextLanguageId, CancellationToken cancellationToken) + { + return GetActiveAlarmsCoreAsync(alarmTextLanguageId, null, cancellationToken); + } + + private Task> GetActiveAlarmsCoreAsync(int alarmTextLanguageId, Func textListResolver, CancellationToken cancellationToken) + { + return ExecuteReadOperationAsync("GetActiveAlarms", session => + { + var error = session.GetActiveAlarms(out var alarmList, alarmTextLanguageId, textListResolver); + ThrowIfError("GetActiveAlarms", error); + return (IReadOnlyList)((alarmList ?? new List()).Where(alarm => alarm != null).ToList()); + }, cancellationToken); + } + + public async Task SubscribeTagsAsync(IEnumerable tags, S7CommPlusSubscriptionOptions options = null, CancellationToken cancellationToken = default) + { + if (tags == null) + { + throw new ArgumentNullException(nameof(tags)); + } + + var tagList = tags.ToList(); + if (tagList.Count == 0) + { + throw new ArgumentException("At least one tag is required.", nameof(tags)); + } + if (tagList.Any(tag => tag == null)) + { + throw new ArgumentException("Tag list cannot contain null entries.", nameof(tags)); + } + + var subscriptionOptions = (options ?? new S7CommPlusSubscriptionOptions()).Clone(); + subscriptionOptions.Validate(requireCycleTime: true); + var tagsByReferenceId = tagList + .Select((tag, index) => new KeyValuePair((uint)(index + 1), tag)) + .ToDictionary(pair => pair.Key, pair => pair.Value); + var subscription = new S7CommPlusTagSubscription(tagsByReferenceId); + + await _operationGate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + ThrowIfDisposed(); + await EnsureConnectedCoreAsync(cancellationToken).ConfigureAwait(false); + uint subscriptionObjectId = 0; + var error = await RunWithTimeoutAsync( + "CreateTagSubscription", + () => _session.CreateTagSubscription(tagList, subscriptionOptions.CycleTimeMilliseconds, subscriptionOptions.InitialCreditLimit, out subscriptionObjectId), + _options.RequestTimeout, + cancellationToken).ConfigureAwait(false); + ThrowIfError("CreateTagSubscription", error); + + subscription.Start(token => RunTagSubscriptionLoopAsync(subscription, subscriptionOptions, subscriptionObjectId, token)); + return subscription; + } + catch (S7CommPlusException ex) + { + RaiseCommunicationError(ex); + if (ex.IsTransient || ex is S7CommPlusTisWatchUnavailableException) + { + SetState(S7CommPlusConnectionState.Faulted, ex); + } + subscription.MarkFaulted(ex); + throw; + } + finally + { + _operationGate.Release(); + } + } + + public async Task OpenBlockOnlineViewAsync(S7CommPlusTisWatchRequest request, S7CommPlusSubscriptionOptions options = null, CancellationToken cancellationToken = default) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + request.Validate(); + var watchRequest = request.Clone(); + var subscriptionOptions = (options ?? new S7CommPlusSubscriptionOptions()).Clone(); + subscriptionOptions.Validate(requireCycleTime: false); + var subscription = new S7CommPlusTisWatchSubscription(watchRequest.ResultModel); + + await _operationGate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + ThrowIfDisposed(); + await EnsureConnectedCoreAsync(cancellationToken).ConfigureAwait(false); + uint subscriptionObjectId = 0; + var error = await RunWithTimeoutAsync( + "CreateTisWatchSubscription", + () => _session.CreateTisWatchSubscription(watchRequest, out subscriptionObjectId), + _options.RequestTimeout, + cancellationToken).ConfigureAwait(false); + var operation = String.IsNullOrWhiteSpace(watchRequest.LastLifecycleStage) + ? "CreateTisWatchSubscription" + : $"CreateTisWatchSubscription ({watchRequest.LastLifecycleStage})"; + ThrowIfError(operation, error); + + subscription.Start(token => RunTisWatchSubscriptionLoopAsync(subscription, subscriptionOptions, subscriptionObjectId, token)); + return subscription; + } + catch (S7CommPlusException ex) + { + RaiseCommunicationError(ex); + if (ex.IsTransient || ex is S7CommPlusTisWatchUnavailableException) + { + SetState(S7CommPlusConnectionState.Faulted, ex); + } + subscription.MarkFaulted(ex); + throw; + } + finally + { + _operationGate.Release(); + } + } + + /// + /// Creates a live alarm subscription and requests all alarm text languages. The legacy + /// property contains the first returned language; + /// use to access every returned language. + /// + public Task SubscribeAlarmsAsync(S7CommPlusSubscriptionOptions options = null, CancellationToken cancellationToken = default) + { + return SubscribeAlarmsAsync(Array.Empty(), 0, options, cancellationToken); + } + + /// + /// Creates a live alarm subscription and requests alarm texts for one LCID. + /// + public Task SubscribeAlarmsAsync(int languageId, S7CommPlusSubscriptionOptions options = null, CancellationToken cancellationToken = default) + { + return SubscribeAlarmsAsync(new[] { languageId }, languageId, options, cancellationToken); + } + + /// + /// Creates a live alarm subscription for one LCID and resolves text-list placeholders using a catalog returned by . + /// + public Task SubscribeAlarmsAsync(int languageId, S7CommPlusTextListCatalog textLists, S7CommPlusSubscriptionOptions options = null, CancellationToken cancellationToken = default) + { + return SubscribeAlarmsAsync(new[] { languageId }, languageId, textLists, options, cancellationToken); + } + + /// + /// Creates a live alarm subscription first, then uses the supplied separate snapshot client to read the + /// initially active alarms with all alarm text languages. Early live notifications are buffered by the subscription. + /// + public Task SubscribeAlarmsWithSnapshotAsync(S7CommPlusClient snapshotClient, S7CommPlusSubscriptionOptions options = null, CancellationToken cancellationToken = default) + { + return SubscribeAlarmsWithSnapshotAsync(snapshotClient, Array.Empty(), 0, options, cancellationToken); + } + + /// + /// Creates a live alarm subscription first, then uses the supplied separate snapshot client to read the + /// initially active alarms for the requested LCID. Early live notifications are buffered by the subscription. + /// + public Task SubscribeAlarmsWithSnapshotAsync(S7CommPlusClient snapshotClient, int languageId, S7CommPlusSubscriptionOptions options = null, CancellationToken cancellationToken = default) + { + return SubscribeAlarmsWithSnapshotAsync(snapshotClient, new[] { languageId }, languageId, options, cancellationToken); + } + + /// + /// Creates a live alarm subscription for one LCID, reads initially active alarms, and resolves text-list placeholders using a catalog returned by . + /// + public Task SubscribeAlarmsWithSnapshotAsync(S7CommPlusClient snapshotClient, int languageId, S7CommPlusTextListCatalog textLists, S7CommPlusSubscriptionOptions options = null, CancellationToken cancellationToken = default) + { + return SubscribeAlarmsWithSnapshotAsync(snapshotClient, new[] { languageId }, languageId, textLists, options, cancellationToken); + } + + /// + /// Creates a live alarm subscription first, then uses the supplied separate snapshot client to read the + /// initially active alarms. Pass an empty language collection to request all alarm text languages. + /// + public async Task SubscribeAlarmsWithSnapshotAsync(S7CommPlusClient snapshotClient, IEnumerable languageIds, int alarmTextLanguageId, S7CommPlusSubscriptionOptions options = null, CancellationToken cancellationToken = default) + { + return await SubscribeAlarmsWithSnapshotAsync(snapshotClient, languageIds, alarmTextLanguageId, null, options, cancellationToken).ConfigureAwait(false); + } + + /// + /// Creates a live alarm subscription first, then uses the supplied separate snapshot client to read the + /// initially active alarms. Pass an empty language collection to request all alarm text languages. + /// Text-list placeholders are resolved through the supplied catalog. + /// + public async Task SubscribeAlarmsWithSnapshotAsync(S7CommPlusClient snapshotClient, IEnumerable languageIds, int alarmTextLanguageId, S7CommPlusTextListCatalog textLists, S7CommPlusSubscriptionOptions options = null, CancellationToken cancellationToken = default) + { + if (snapshotClient == null) + { + throw new ArgumentNullException(nameof(snapshotClient)); + } + if (ReferenceEquals(this, snapshotClient)) + { + throw new ArgumentException("The snapshot client must be a separate S7CommPlusClient instance.", nameof(snapshotClient)); + } + + var subscription = await SubscribeAlarmsAsync(languageIds, alarmTextLanguageId, textLists, options, cancellationToken).ConfigureAwait(false); + try + { + var activeAlarms = await snapshotClient.GetActiveAlarmsCoreAsync(alarmTextLanguageId, CreateTextListResolver(textLists), cancellationToken).ConfigureAwait(false); + return new S7CommPlusAlarmSubscriptionWithSnapshot(activeAlarms, subscription); + } + catch + { + await subscription.DisposeAsync().ConfigureAwait(false); + throw; + } + } + + /// + /// Creates a live alarm subscription. Pass an empty language collection to request all alarm text languages. + /// The selects the language exposed through the legacy + /// property; use 0 to expose the first returned language there and + /// inspect for the full set. + /// + public async Task SubscribeAlarmsAsync(IEnumerable languageIds, int alarmTextLanguageId, S7CommPlusSubscriptionOptions options = null, CancellationToken cancellationToken = default) + { + return await SubscribeAlarmsAsync(languageIds, alarmTextLanguageId, null, options, cancellationToken).ConfigureAwait(false); + } + + /// + /// Creates a live alarm subscription. Pass an empty language collection to request all alarm text languages. + /// Text-list placeholders are resolved through the supplied catalog. + /// + public async Task SubscribeAlarmsAsync(IEnumerable languageIds, int alarmTextLanguageId, S7CommPlusTextListCatalog textLists, S7CommPlusSubscriptionOptions options = null, CancellationToken cancellationToken = default) + { + var languageIdList = languageIds?.ToList() ?? new List(); + if (languageIdList.Any(languageId => languageId < 0)) + { + throw new ArgumentOutOfRangeException(nameof(languageIds), "Language ids must be positive LCID values."); + } + if (alarmTextLanguageId < 0) + { + throw new ArgumentOutOfRangeException(nameof(alarmTextLanguageId), "Language ids must be positive LCID values."); + } + + var subscriptionOptions = (options ?? new S7CommPlusSubscriptionOptions()).Clone(); + subscriptionOptions.Validate(requireCycleTime: false); + var subscription = new S7CommPlusAlarmSubscription(alarmTextLanguageId, CreateTextListResolver(textLists)); + + await _operationGate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + ThrowIfDisposed(); + await EnsureConnectedCoreAsync(cancellationToken).ConfigureAwait(false); + var languageIdsUint = languageIdList.Select(languageId => checked((uint)languageId)).ToArray(); + uint subscriptionObjectId = 0; + var error = await RunWithTimeoutAsync( + "CreateAlarmSubscription", + () => _session.CreateAlarmSubscription(languageIdsUint, subscriptionOptions.InitialCreditLimit, out subscriptionObjectId), + _options.RequestTimeout, + cancellationToken).ConfigureAwait(false); + ThrowIfError("CreateAlarmSubscription", error); + + subscription.Start(token => RunAlarmSubscriptionLoopAsync(subscription, subscriptionOptions, subscriptionObjectId, token)); + return subscription; + } + catch (S7CommPlusException ex) + { + RaiseCommunicationError(ex); + if (ex.IsTransient) + { + SetState(S7CommPlusConnectionState.Faulted, ex); + } + subscription.MarkFaulted(ex); + throw; + } + finally + { + _operationGate.Release(); + } + } + + public Task GetTagBySymbolAsync(string symbol, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(symbol)) + { + throw new ArgumentException("Symbol is required.", nameof(symbol)); + } + + return ExecuteReadOperationAsync("GetTagBySymbol", session => + { + var tag = session.GetPlcTagBySymbol(symbol); + if (tag == null) + { + throw new S7CommPlusConnectionException("GetTagBySymbol", Endpoint, S7Consts.errCliItemNotAvailable, false, $"PLC tag '{symbol}' could not be resolved."); + } + return tag; + }, cancellationToken); + } + + public Task> ReadAsync(IEnumerable addresses, CancellationToken cancellationToken = default) + { + if (addresses == null) + { + throw new ArgumentNullException(nameof(addresses)); + } + + var addressList = addresses.ToList(); + if (addressList.Count == 0) + { + return Task.FromResult(new S7CommPlusBatchResult(Array.Empty())); + } + + return ExecuteReadOperationAsync("Read", session => + { + var error = session.ReadValues(addressList, out var values, out var itemErrors); + ThrowIfError("Read", error); + var items = new List(addressList.Count); + for (var i = 0; i < addressList.Count; i++) + { + var value = i < values.Count ? values[i] : null; + var itemError = i < itemErrors.Count ? itemErrors[i] : ulong.MaxValue; + items.Add(new S7CommPlusReadResult(addressList[i], value, itemError)); + } + return new S7CommPlusBatchResult(items); + }, cancellationToken); + } + + public Task> ReadAsync(IEnumerable tags, CancellationToken cancellationToken = default) + { + if (tags == null) + { + throw new ArgumentNullException(nameof(tags)); + } + + var tagList = tags.ToList(); + if (tagList.Any(tag => tag == null)) + { + throw new ArgumentException("Tag list cannot contain null entries.", nameof(tags)); + } + if (tagList.Count == 0) + { + return Task.FromResult(new S7CommPlusBatchResult(Array.Empty())); + } + + return ExecuteReadOperationAsync("ReadTags", session => + { + var addresses = tagList.Select(tag => tag.Address).ToList(); + var error = session.ReadValues(addresses, out var values, out var itemErrors); + ThrowIfError("ReadTags", error); + var items = new List(tagList.Count); + for (var i = 0; i < tagList.Count; i++) + { + var value = i < values.Count ? values[i] : null; + var itemError = i < itemErrors.Count ? itemErrors[i] : ulong.MaxValue; + tagList[i].ProcessReadResult(value, itemError); + items.Add(new S7CommPlusTagReadResult(tagList[i], itemError)); + } + return new S7CommPlusBatchResult(items); + }, cancellationToken); + } + + internal Task> WriteAsync(IEnumerable addresses, IEnumerable values, CancellationToken cancellationToken = default) + { + if (addresses == null) + { + throw new ArgumentNullException(nameof(addresses)); + } + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + var addressList = addresses.ToList(); + var valueList = values.ToList(); + if (addressList.Count != valueList.Count) + { + throw new ArgumentException("Address and value counts must match.", nameof(values)); + } + if (addressList.Count == 0) + { + return Task.FromResult(new S7CommPlusBatchResult(Array.Empty())); + } + + return ExecuteWriteOperationAsync("Write", session => + { + var error = session.WriteValues(addressList, valueList, out var itemErrors); + ThrowIfError("Write", error); + var items = new List(addressList.Count); + for (var i = 0; i < addressList.Count; i++) + { + var itemError = i < itemErrors.Count ? itemErrors[i] : ulong.MaxValue; + items.Add(new S7CommPlusWriteResult(addressList[i], itemError)); + } + return new S7CommPlusBatchResult(items); + }, cancellationToken); + } + + public Task> WriteAsync(IEnumerable tags, CancellationToken cancellationToken = default) + { + if (tags == null) + { + throw new ArgumentNullException(nameof(tags)); + } + + var tagList = tags.ToList(); + if (tagList.Any(tag => tag == null)) + { + throw new ArgumentException("Tag list cannot contain null entries.", nameof(tags)); + } + if (tagList.Count == 0) + { + return Task.FromResult(new S7CommPlusBatchResult(Array.Empty())); + } + + return ExecuteWriteOperationAsync("WriteTags", session => + { + var addresses = tagList.Select(tag => tag.Address).ToList(); + var values = tagList.Select(tag => tag.GetWriteValue()).ToList(); + var error = session.WriteValues(addresses, values, out var itemErrors); + ThrowIfError("WriteTags", error); + var items = new List(tagList.Count); + for (var i = 0; i < tagList.Count; i++) + { + var itemError = i < itemErrors.Count ? itemErrors[i] : ulong.MaxValue; + tagList[i].ProcessWriteResult(itemError); + items.Add(new S7CommPlusWriteResult(addresses[i], itemError)); + } + return new S7CommPlusBatchResult(items); + }, cancellationToken); + } + + public async ValueTask DisposeAsync() + { + if (_disposed) + { + return; + } + + try + { + await DisconnectAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + _options.Logger.LogWarning(ex, "Error while disposing S7CommPlusClient for {Endpoint}.", Endpoint); + } + finally + { + _disposed = true; + _operationGate.Dispose(); + } + } + + private Task ExecuteReadOperationAsync(string operation, Func operationFunc, CancellationToken cancellationToken) + { + return ExecuteOperationAsync(operation, allowReconnect: true, operationFunc, cancellationToken); + } + + private Task ExecuteSessionOperationAsync(string operation, Func operationFunc, CancellationToken cancellationToken) + { + return ExecuteOperationAsync(operation, allowReconnect: false, operationFunc, cancellationToken); + } + + private async Task SetCpuOperatingStateAsync(string operation, int operatingStateRequest, CancellationToken cancellationToken) + { + await ExecuteWriteOperationAsync(operation, session => + { + var error = session.SetCpuOperatingState(operatingStateRequest); + ThrowIfError(operation, error); + return true; + }, cancellationToken).ConfigureAwait(false); + } + + private Task ExecuteWriteOperationAsync(string operation, Func operationFunc, CancellationToken cancellationToken) + { + if (!_options.WriteEnabled) + { + throw new S7CommPlusWriteDisabledException(Endpoint); + } + return ExecuteOperationAsync(operation, allowReconnect: false, operationFunc, cancellationToken); + } + + private async Task ExecuteOperationAsync(string operation, bool allowReconnect, Func operationFunc, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + await _operationGate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + await EnsureConnectedCoreAsync(cancellationToken).ConfigureAwait(false); + try + { + return await RunWithTimeoutAsync(operation, () => operationFunc(_session), _options.RequestTimeout, cancellationToken).ConfigureAwait(false); + } + catch (S7CommPlusException ex) when (allowReconnect && _options.AutoReconnect && ex.IsTransient) + { + RaiseCommunicationError(ex); + _options.Logger.LogWarning(ex, "Transient {Operation} failure for {Endpoint}; reconnecting and retrying once.", operation, Endpoint); + await ReconnectCoreAsync(cancellationToken).ConfigureAwait(false); + return await RunWithTimeoutAsync(operation, () => operationFunc(_session), _options.RequestTimeout, cancellationToken).ConfigureAwait(false); + } + } + catch (S7CommPlusException ex) + { + RaiseCommunicationError(ex); + if (ex.IsTransient) + { + SetState(S7CommPlusConnectionState.Faulted, ex); + } + throw; + } + finally + { + _operationGate.Release(); + } + } + + private async Task EnsureConnectedCoreAsync(CancellationToken cancellationToken) + { + if (_session?.IsConnected == true && _state == S7CommPlusConnectionState.Connected) + { + return; + } + + await ConnectCoreAsync(S7CommPlusConnectionState.Connecting, cancellationToken).ConfigureAwait(false); + } + + private async Task ConnectCoreAsync(S7CommPlusConnectionState connectingState, CancellationToken cancellationToken) + { + if (_session?.IsConnected == true && _state == S7CommPlusConnectionState.Connected) + { + return; + } + + SetState(connectingState); + _session = _sessionFactory(); + _options.Logger.LogInformation("Connecting to PLC {Endpoint}.", Endpoint); + + var error = await RunWithTimeoutAsync("Connect", () => _session.Connect(_options), _options.ConnectTimeout, cancellationToken).ConfigureAwait(false); + if (error != 0) + { + var exception = CreateException("Connect", error); + _session = null; + throw exception; + } + + SetState(S7CommPlusConnectionState.Connected); + _options.Logger.LogInformation("Connected to PLC {Endpoint}.", Endpoint); + } + + private async Task ReconnectCoreAsync(CancellationToken cancellationToken) + { + SetState(S7CommPlusConnectionState.Reconnecting); + await DisconnectCoreAsync(cancellationToken).ConfigureAwait(false); + await ConnectCoreAsync(S7CommPlusConnectionState.Reconnecting, cancellationToken).ConfigureAwait(false); + } + + private async Task DisconnectCoreAsync(CancellationToken cancellationToken) + { + if (_session == null && _state == S7CommPlusConnectionState.Disconnected) + { + return; + } + + SetState(S7CommPlusConnectionState.Disconnecting); + var session = _session; + _session = null; + if (session != null) + { + try + { + var error = await RunWithTimeoutAsync("Disconnect", () => session.Disconnect(_options.DisconnectTimeoutMilliseconds), _options.DisconnectTimeout, cancellationToken).ConfigureAwait(false); + if (error != 0) + { + var exception = CreateException("Disconnect", error); + RaiseCommunicationError(exception); + _options.Logger.LogWarning("PLC disconnect for {Endpoint} returned error {ErrorCode}.", Endpoint, error); + } + } + catch (S7CommPlusException ex) + { + RaiseCommunicationError(ex); + _options.Logger.LogWarning(ex, "PLC disconnect for {Endpoint} failed or timed out.", Endpoint); + } + } + SetState(S7CommPlusConnectionState.Disconnected); + } + + private async Task RunTagSubscriptionLoopAsync(S7CommPlusTagSubscription subscription, S7CommPlusSubscriptionOptions subscriptionOptions, uint subscriptionObjectId, CancellationToken cancellationToken) + { + try + { + await RunSubscriptionLoopAsync( + "WaitForTagSubscriptionNotifications", + subscription, + subscriptionOptions, + waitFunc: () => + { + var error = _session.WaitForTagSubscriptionNotifications( + subscriptionObjectId, + subscriptionOptions.NotificationTimeoutMilliseconds, + subscriptionOptions.CreditLimitStep, + out var notifications); + return (error, notifications); + }, + publish: notification => subscription.Publish(notification)).ConfigureAwait(false); + } + finally + { + await TryDeleteSubscriptionAsync("DeleteTagSubscription", subscription, subscriptionOptions, () => _session.DeleteTagSubscription(subscriptionObjectId)).ConfigureAwait(false); + } + } + + private async Task RunAlarmSubscriptionLoopAsync(S7CommPlusAlarmSubscription subscription, S7CommPlusSubscriptionOptions subscriptionOptions, uint subscriptionObjectId, CancellationToken cancellationToken) + { + try + { + await RunSubscriptionLoopAsync( + "WaitForAlarmNotifications", + subscription, + subscriptionOptions, + waitFunc: () => + { + var error = _session.WaitForAlarmNotifications( + subscriptionObjectId, + subscriptionOptions.NotificationTimeoutMilliseconds, + subscriptionOptions.CreditLimitStep, + out var notifications); + return (error, notifications); + }, + publish: notification => subscription.Publish(notification)).ConfigureAwait(false); + } + finally + { + await TryDeleteSubscriptionAsync("DeleteAlarmSubscription", subscription, subscriptionOptions, () => _session.DeleteAlarmSubscription(subscriptionObjectId)).ConfigureAwait(false); + } + } + + private async Task RunTisWatchSubscriptionLoopAsync(S7CommPlusTisWatchSubscription subscription, S7CommPlusSubscriptionOptions subscriptionOptions, uint subscriptionObjectId, CancellationToken cancellationToken) + { + try + { + await RunTisWatchLoopAsync( + "WaitForTisWatchNotifications", + subscription, + subscriptionOptions, + waitFunc: () => + { + var error = _session.WaitForTisWatchNotifications( + subscriptionObjectId, + subscriptionOptions.NotificationTimeoutMilliseconds, + out var notifications); + return (error, notifications); + }, + publish: notification => subscription.Publish(notification)).ConfigureAwait(false); + } + finally + { + await TryDeleteSubscriptionAsync("DeleteTisWatchSubscription", subscription, subscriptionOptions, () => _session.DeleteTisWatchSubscription(subscriptionObjectId)).ConfigureAwait(false); + } + } + + private async Task RunSubscriptionLoopAsync( + string operation, + S7CommPlusSubscription subscription, + S7CommPlusSubscriptionOptions subscriptionOptions, + Func<(int Error, List Notifications)> waitFunc, + Action publish) + { + var consecutiveTimeouts = 0; + while (!subscription.IsStopRequested) + { + try + { + var result = await RunWithTimeoutAsync( + operation, + waitFunc, + subscriptionOptions.NotificationTimeout + TimeSpan.FromSeconds(1), + CancellationToken.None).ConfigureAwait(false); + + if (result.Error == S7Consts.errCliJobTimeout || result.Error == S7Consts.errTCPReceiveTimeout) + { + consecutiveTimeouts++; + if (subscriptionOptions.MaxConsecutiveTimeoutsBeforeFault > 0 + && consecutiveTimeouts >= subscriptionOptions.MaxConsecutiveTimeoutsBeforeFault) + { + throw CreateException(operation, result.Error); + } + continue; + } + + ThrowIfError(operation, result.Error); + consecutiveTimeouts = 0; + + foreach (var notification in result.Notifications ?? Enumerable.Empty()) + { + publish(notification); + } + } + catch (S7CommPlusException ex) + { + RaiseCommunicationError(ex); + if (ex.IsTransient) + { + SetState(S7CommPlusConnectionState.Faulted, ex); + } + subscription.MarkFaulted(ex); + throw; + } + catch (Exception ex) + { + var wrapped = new S7CommPlusConnectionException(operation, Endpoint, S7Consts.errCliFunctionRefused, true, $"{operation} failed for PLC {Endpoint}.", ex); + RaiseCommunicationError(wrapped); + SetState(S7CommPlusConnectionState.Faulted, wrapped); + subscription.MarkFaulted(wrapped); + throw wrapped; + } + } + } + + private async Task RunTisWatchLoopAsync( + string operation, + S7CommPlusSubscription subscription, + S7CommPlusSubscriptionOptions subscriptionOptions, + Func<(int Error, List Notifications)> waitFunc, + Action publish) + { + var consecutiveTimeouts = 0; + while (!subscription.IsStopRequested) + { + try + { + var result = await RunWithTimeoutAsync( + operation, + waitFunc, + subscriptionOptions.NotificationTimeout + _options.RequestTimeout + TimeSpan.FromSeconds(1), + CancellationToken.None).ConfigureAwait(false); + + if (result.Error == S7Consts.errCliJobTimeout || result.Error == S7Consts.errTCPReceiveTimeout) + { + consecutiveTimeouts++; + if (subscriptionOptions.MaxConsecutiveTimeoutsBeforeFault > 0 + && consecutiveTimeouts >= subscriptionOptions.MaxConsecutiveTimeoutsBeforeFault) + { + throw CreateTisWatchException(operation, result.Error); + } + continue; + } + + ThrowIfTisWatchError(operation, result.Error); + consecutiveTimeouts = 0; + + foreach (var notification in result.Notifications ?? Enumerable.Empty()) + { + publish(notification); + } + } + catch (S7CommPlusException ex) + { + RaiseCommunicationError(ex); + if (ex.IsTransient) + { + SetState(S7CommPlusConnectionState.Faulted, ex); + } + subscription.MarkFaulted(ex); + throw; + } + catch (Exception ex) + { + var wrapped = new S7CommPlusConnectionException(operation, Endpoint, S7Consts.errCliFunctionRefused, true, $"{operation} failed for PLC {Endpoint}.", ex); + RaiseCommunicationError(wrapped); + SetState(S7CommPlusConnectionState.Faulted, wrapped); + subscription.MarkFaulted(wrapped); + throw wrapped; + } + } + } + + private void ThrowIfTisWatchError(string operation, int errorCode) + { + if (errorCode == 0) + { + return; + } + + throw CreateTisWatchException(operation, errorCode); + } + + private S7CommPlusException CreateTisWatchException(string operation, int errorCode) + { + var diagnostic = _session?.LastTisWatchDiagnostic; + var effectiveOperation = string.IsNullOrWhiteSpace(diagnostic) + ? operation + : $"{operation}: {diagnostic}"; + return CreateException(effectiveOperation, errorCode); + } + + private async Task TryDeleteSubscriptionAsync(string operation, S7CommPlusSubscription subscription, S7CommPlusSubscriptionOptions subscriptionOptions, Func deleteFunc) + { + if (!subscriptionOptions.DeleteOnStop || _session == null) + { + return; + } + if (subscription.FaultException != null && S7CommPlusErrorClassifier.IsConnectionDefinitelyClosed(subscription.FaultException.ErrorCode)) + { + _options.Logger.LogDebug( + "Skipping {Operation} for {Endpoint} because the subscription already observed connection loss {ErrorCode}.", + operation, + Endpoint, + subscription.FaultException.ErrorCode); + return; + } + + try + { + var error = await RunWithTimeoutAsync(operation, deleteFunc, _options.DisconnectTimeout, CancellationToken.None).ConfigureAwait(false); + if (error != 0) + { + var exception = CreateException(operation, error); + RaiseCommunicationError(exception); + subscription.MarkFaulted(exception); + _options.Logger.LogWarning("PLC subscription delete for {Endpoint} returned error {ErrorCode}.", Endpoint, error); + } + } + catch (S7CommPlusException ex) + { + RaiseCommunicationError(ex); + subscription.MarkFaulted(ex); + _options.Logger.LogWarning(ex, "PLC subscription delete for {Endpoint} failed or timed out.", Endpoint); + } + } + + private async Task RunWithTimeoutAsync(string operation, Func func, TimeSpan timeout, CancellationToken cancellationToken) + { + try + { + return await Task.Run(func).WaitAsync(timeout, cancellationToken).ConfigureAwait(false); + } + catch (TimeoutException ex) + { + throw new S7CommPlusTimeoutException(operation, Endpoint, S7Consts.errCliJobTimeout, $"{operation} timed out after {timeout}.", ex); + } + catch (S7CommPlusException) + { + throw; + } + catch (Exception ex) + { + throw new S7CommPlusConnectionException(operation, Endpoint, S7Consts.errCliFunctionRefused, true, $"{operation} failed for PLC {Endpoint}.", ex); + } + } + + private void ThrowIfError(string operation, int errorCode) + { + if (errorCode != 0) + { + throw CreateException(operation, errorCode); + } + } + + private S7CommPlusException CreateException(string operation, int errorCode) + => S7CommPlusErrorClassifier.CreateException(operation, Endpoint, errorCode, _session?.LastErrorDetail); + + private void RaiseCommunicationError(S7CommPlusException exception) + { + CommunicationError?.Invoke(this, new S7CommPlusCommunicationErrorEventArgs(exception)); + } + + private void SetState(S7CommPlusConnectionState newState, Exception exception = null) + { + var oldState = _state; + if (oldState == newState) + { + return; + } + + _state = newState; + ConnectionStateChanged?.Invoke(this, new S7CommPlusConnectionStateChangedEventArgs(oldState, newState, exception)); + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(S7CommPlusClient)); + } + } + + private static Func CreateTextListResolver(S7CommPlusTextListCatalog textLists) + { + return textLists == null ? null : new Func(textLists.ResolveText); + } + + private string Endpoint => $"{_options.Address}:{_options.Port}"; + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusClientBlockContent.cs b/src/S7CommPlusDriver/S7CommPlusClientBlockContent.cs new file mode 100644 index 0000000..5db10d5 --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusClientBlockContent.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace S7CommPlusDriver +{ + public sealed record S7CommPlusClientBlockContent( + uint RelationId, + string Name, + S7CommPlusProgrammingLanguage Language, + uint Number, + S7CommPlusBlockType Type, + string XmlLineComment, + IReadOnlyDictionary XmlComments, + string InterfaceDescription, + IReadOnlyList BlockBody, + string FunctionalObjectCode, + string FunctionalObjectDebugInfo, + IReadOnlyList InternalReferences, + IReadOnlyList ExternalReferences, + byte[] FunctionalObjectCodeBytes = null, + byte[] CodeModifiedTimestampBytes = null, + IReadOnlyDictionary BinaryArtifacts = null, + IReadOnlyDictionary OnlineMetadata = null, + IReadOnlyDictionary NetworkComments = null, + IReadOnlyDictionary NetworkTitles = null); +} diff --git a/src/S7CommPlusDriver/S7CommPlusClientEvents.cs b/src/S7CommPlusDriver/S7CommPlusClientEvents.cs new file mode 100644 index 0000000..24db1d0 --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusClientEvents.cs @@ -0,0 +1,38 @@ +using System; + +namespace S7CommPlusDriver +{ + public enum S7CommPlusConnectionState + { + Disconnected, + Connecting, + Connected, + Reconnecting, + Disconnecting, + Faulted + } + + public sealed class S7CommPlusConnectionStateChangedEventArgs : EventArgs + { + public S7CommPlusConnectionStateChangedEventArgs(S7CommPlusConnectionState oldState, S7CommPlusConnectionState newState, Exception exception = null) + { + OldState = oldState; + NewState = newState; + Exception = exception; + } + + public S7CommPlusConnectionState OldState { get; } + public S7CommPlusConnectionState NewState { get; } + public Exception Exception { get; } + } + + public sealed class S7CommPlusCommunicationErrorEventArgs : EventArgs + { + public S7CommPlusCommunicationErrorEventArgs(S7CommPlusException exception) + { + Exception = exception ?? throw new ArgumentNullException(nameof(exception)); + } + + public S7CommPlusException Exception { get; } + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusClientOptions.cs b/src/S7CommPlusDriver/S7CommPlusClientOptions.cs new file mode 100644 index 0000000..bf97c4f --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusClientOptions.cs @@ -0,0 +1,99 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using S7CommPlusDriver.Internal; +using System; +using System.Text; + +namespace S7CommPlusDriver +{ + public sealed class S7CommPlusClientOptions + { + public string Address { get; set; } = string.Empty; + public int Port { get; set; } = S7CommPlusDefaults.IsoTcpPort; + public ushort LocalTsap { get; set; } = S7CommPlusDefaults.LocalTsap; + public string RemoteTsap { get; set; } = S7CommPlusDefaults.RemoteTsapHmi; + public string Password { get; set; } = string.Empty; + public string Username { get; set; } = string.Empty; + public TimeSpan ConnectTimeout { get; set; } = S7CommPlusDefaults.ConnectTimeout; + public TimeSpan RequestTimeout { get; set; } = S7CommPlusDefaults.RequestTimeout; + public TimeSpan DisconnectTimeout { get; set; } = S7CommPlusDefaults.DisconnectTimeout; + public bool AutoReconnect { get; set; } = true; + public bool WriteEnabled { get; set; } = false; + public S7CommPlusSecurityMode SecurityMode { get; set; } = S7CommPlusSecurityMode.Tls; + public S7CommPlusTlsBackend TlsBackend { get; set; } = S7CommPlusTlsBackend.BouncyCastle; + public S7CommPlusSecurityMode? NegotiatedSecurityMode { get; internal set; } + public Func LegacyPublicKeyResolver { get; set; } + public ILogger Logger { get; set; } = NullLogger.Instance; + + internal int ConnectTimeoutMilliseconds => ToPositiveMilliseconds(ConnectTimeout, nameof(ConnectTimeout)); + internal int RequestTimeoutMilliseconds => ToPositiveMilliseconds(RequestTimeout, nameof(RequestTimeout)); + internal int DisconnectTimeoutMilliseconds => ToPositiveMilliseconds(DisconnectTimeout, nameof(DisconnectTimeout)); + internal byte[] RemoteTsapBytes => Encoding.ASCII.GetBytes(RemoteTsap ?? string.Empty); + + internal S7CommPlusClientOptions Clone() + { + return (S7CommPlusClientOptions)MemberwiseClone(); + } + + internal void Validate() + { + if (string.IsNullOrWhiteSpace(Address)) + { + throw new ArgumentException("PLC address is required.", nameof(Address)); + } + if (Port <= 0 || Port > 65535) + { + throw new ArgumentOutOfRangeException(nameof(Port), "Port must be between 1 and 65535."); + } + if (string.IsNullOrWhiteSpace(RemoteTsap)) + { + throw new ArgumentException("Remote TSAP is required.", nameof(RemoteTsap)); + } + foreach (var character in RemoteTsap) + { + if (character > 0x7F) + { + throw new ArgumentException("Remote TSAP must contain only ASCII characters.", nameof(RemoteTsap)); + } + } + if (RemoteTsapBytes.Length > S7CommPlusProtocolConstants.MaxCotpParameterLength) + { + throw new ArgumentOutOfRangeException(nameof(RemoteTsap), $"Remote TSAP must be {S7CommPlusProtocolConstants.MaxCotpParameterLength} bytes or shorter."); + } + if (!Enum.IsDefined(typeof(S7CommPlusSecurityMode), SecurityMode)) + { + throw new ArgumentOutOfRangeException(nameof(SecurityMode), "Security mode is not supported."); + } + if (!Enum.IsDefined(typeof(S7CommPlusTlsBackend), TlsBackend)) + { + throw new ArgumentOutOfRangeException(nameof(TlsBackend), "TLS backend is not supported."); + } +#if !NET8_0_OR_GREATER + if (SecurityMode != S7CommPlusSecurityMode.Tls) + { + throw new S7CommPlusUnsupportedSecurityModeException( + SecurityMode, + $"{Address}:{Port}", + "Legacy S7CommPlus challenge authentication is available only on net8.0 and later builds."); + } +#endif + _ = ConnectTimeoutMilliseconds; + _ = RequestTimeoutMilliseconds; + _ = DisconnectTimeoutMilliseconds; + Logger ??= NullLogger.Instance; + } + + private static int ToPositiveMilliseconds(TimeSpan value, string name) + { + if (value <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(name, "Timeout must be greater than zero."); + } + if (value.TotalMilliseconds > int.MaxValue) + { + throw new ArgumentOutOfRangeException(name, "Timeout is too large."); + } + return Math.Max(1, (int)value.TotalMilliseconds); + } + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusCommunicationResources.cs b/src/S7CommPlusDriver/S7CommPlusCommunicationResources.cs new file mode 100644 index 0000000..386d2a1 --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusCommunicationResources.cs @@ -0,0 +1,26 @@ +namespace S7CommPlusDriver +{ + public sealed class S7CommPlusCommunicationResources + { + internal S7CommPlusCommunicationResources(S7CommPlusCommunicationResourceSnapshot resources) + { + TagsPerReadRequestMax = resources.TagsPerReadRequestMax; + TagsPerWriteRequestMax = resources.TagsPerWriteRequestMax; + PlcAttributesMax = resources.PlcAttributesMax; + PlcAttributesFree = resources.PlcAttributesFree; + PlcSubscriptionsMax = resources.PlcSubscriptionsMax; + PlcSubscriptionsFree = resources.PlcSubscriptionsFree; + SubscriptionMemoryMax = resources.SubscriptionMemoryMax; + SubscriptionMemoryFree = resources.SubscriptionMemoryFree; + } + + public int TagsPerReadRequestMax { get; } + public int TagsPerWriteRequestMax { get; } + public int PlcAttributesMax { get; } + public int PlcAttributesFree { get; } + public int PlcSubscriptionsMax { get; } + public int PlcSubscriptionsFree { get; } + public int SubscriptionMemoryMax { get; } + public int SubscriptionMemoryFree { get; } + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusCpuCultureInfo.cs b/src/S7CommPlusDriver/S7CommPlusCpuCultureInfo.cs new file mode 100644 index 0000000..ed615e2 --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusCpuCultureInfo.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace S7CommPlusDriver +{ + public sealed class S7CommPlusCpuCultureInfo + { + internal S7CommPlusCpuCultureInfo(IEnumerable languageIds) + { + LanguageIds = (languageIds ?? Enumerable.Empty()).Distinct().ToList(); + Cultures = LanguageIds + .Select(TryGetCultureInfo) + .Where(culture => culture != null) + .ToList(); + UnresolvedLanguageIds = LanguageIds + .Where(languageId => TryGetCultureInfo(languageId) == null) + .ToList(); + } + + public IReadOnlyList LanguageIds { get; } + public IReadOnlyList Cultures { get; } + public IReadOnlyList UnresolvedLanguageIds { get; } + public int? PrimaryLanguageId => LanguageIds.Count > 0 ? LanguageIds[0] : (int?)null; + public CultureInfo PrimaryCulture => PrimaryLanguageId.HasValue ? TryGetCultureInfo(PrimaryLanguageId.Value) : null; + + private static CultureInfo TryGetCultureInfo(int languageId) + { + try + { + return CultureInfo.GetCultureInfo(languageId); + } + catch (CultureNotFoundException) + { + return null; + } + } + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusCpuCycleTime.cs b/src/S7CommPlusDriver/S7CommPlusCpuCycleTime.cs new file mode 100644 index 0000000..e5b1c49 --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusCpuCycleTime.cs @@ -0,0 +1,25 @@ +namespace S7CommPlusDriver +{ + public sealed class S7CommPlusCpuCycleTime + { + internal S7CommPlusCpuCycleTime( + double? configuredMinimumMilliseconds, + double? configuredMaximumMilliseconds, + double shortestMilliseconds, + double currentMilliseconds, + double longestMilliseconds) + { + ConfiguredMinimumMilliseconds = configuredMinimumMilliseconds; + ConfiguredMaximumMilliseconds = configuredMaximumMilliseconds; + ShortestMilliseconds = shortestMilliseconds; + CurrentMilliseconds = currentMilliseconds; + LongestMilliseconds = longestMilliseconds; + } + + public double? ConfiguredMinimumMilliseconds { get; } + public double? ConfiguredMaximumMilliseconds { get; } + public double ShortestMilliseconds { get; } + public double CurrentMilliseconds { get; } + public double LongestMilliseconds { get; } + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusCpuMemoryUsage.cs b/src/S7CommPlusDriver/S7CommPlusCpuMemoryUsage.cs new file mode 100644 index 0000000..eb968e5 --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusCpuMemoryUsage.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; + +namespace S7CommPlusDriver +{ + public sealed class S7CommPlusCpuMemoryUsage + { + internal S7CommPlusCpuMemoryUsage(IReadOnlyList areas) + { + Areas = areas ?? Array.Empty(); + } + + public IReadOnlyList Areas { get; } + } + + public sealed class S7CommPlusCpuMemoryArea + { + internal S7CommPlusCpuMemoryArea(string key, string name, long totalBytes, long usedBytes) + { + Key = key ?? string.Empty; + Name = name ?? string.Empty; + TotalBytes = Math.Max(0, totalBytes); + UsedBytes = Math.Max(0, usedBytes); + } + + public string Key { get; } + public string Name { get; } + public long TotalBytes { get; } + public long UsedBytes { get; } + public long FreeBytes => Math.Max(0, TotalBytes - UsedBytes); + public double? UsedPercent => TotalBytes > 0 ? Math.Min(100.0, UsedBytes * 100.0 / TotalBytes) : (double?)null; + public double? FreePercent => TotalBytes > 0 ? Math.Max(0.0, FreeBytes * 100.0 / TotalBytes) : (double?)null; + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusCpuOperatingState.cs b/src/S7CommPlusDriver/S7CommPlusCpuOperatingState.cs new file mode 100644 index 0000000..0659e03 --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusCpuOperatingState.cs @@ -0,0 +1,13 @@ +namespace S7CommPlusDriver +{ + public enum S7CommPlusCpuOperatingState + { + Unknown = 0, + NotSupported = 1, + Stop = 2, + Startup = 3, + Run = 4, + Halt = 5, + Defective = 6 + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusCpuState.cs b/src/S7CommPlusDriver/S7CommPlusCpuState.cs new file mode 100644 index 0000000..34a71c2 --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusCpuState.cs @@ -0,0 +1,20 @@ +namespace S7CommPlusDriver +{ + public sealed class S7CommPlusCpuState + { + internal S7CommPlusCpuState(int rawOperatingState, S7CommPlusCpuOperatingState operatingState, int? rawStateSwitch = null, string stateSwitch = null) + { + RawOperatingState = rawOperatingState; + OperatingState = operatingState; + RawStateSwitch = rawStateSwitch; + StateSwitch = stateSwitch; + } + + public int RawOperatingState { get; } + public S7CommPlusCpuOperatingState OperatingState { get; } + public int? RawStateSwitch { get; } + public string StateSwitch { get; } + public bool IsRun => OperatingState == S7CommPlusCpuOperatingState.Run; + public bool IsStop => OperatingState == S7CommPlusCpuOperatingState.Stop; + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusDefaults.cs b/src/S7CommPlusDriver/S7CommPlusDefaults.cs new file mode 100644 index 0000000..bca1aeb --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusDefaults.cs @@ -0,0 +1,16 @@ +using System; + +namespace S7CommPlusDriver +{ + public static class S7CommPlusDefaults + { + public const int IsoTcpPort = 102; + public const ushort LocalTsap = 0x0600; + public const string RemoteTsapHmi = "SIMATIC-ROOT-HMI"; + public const string RemoteTsapEs = "SIMATIC-ROOT-ES"; + + public static readonly TimeSpan ConnectTimeout = TimeSpan.FromSeconds(5); + public static readonly TimeSpan RequestTimeout = TimeSpan.FromSeconds(5); + public static readonly TimeSpan DisconnectTimeout = TimeSpan.FromSeconds(2); + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusDriver.csproj b/src/S7CommPlusDriver/S7CommPlusDriver.csproj index e64d62c..4d24ba1 100644 --- a/src/S7CommPlusDriver/S7CommPlusDriver.csproj +++ b/src/S7CommPlusDriver/S7CommPlusDriver.csproj @@ -1,148 +1,67 @@ - - - + - Debug - AnyCPU - {781951D8-80DE-4E2F-92CA-A5FEF0059415} + net6.0;net8.0;net9.0 Library - S7CommPlusDriver - S7CommPlusDriver - v4.7.2 - 512 - true - true + false + 10.0 - - true - bin\x64\Debug\ - TRACE;DEBUG;_WIN64 - full - x64 - 7.3 - prompt - - - bin\x64\Release\ - TRACE;_WIN64 - true - pdbonly - x64 - 7.3 - prompt - - - true - bin\x86\Debug\ - TRACE;DEBUG;_WIN32 - full - x86 - 7.3 - prompt - - - bin\x86\Release\ - TRACE;_WIN32 - true - pdbonly - x86 - 7.3 - prompt - - - - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + PreserveNewest + true + runtimes/ + - - - + - - {0c89b7a2-a218-49e4-b545-5b044a45f977} - Zlib.net - + + - - + + + + + + 1.1.2 + + + 1.1.2 + + + + + 1.1.2 + + + 1.1.2 + - - xcopy /y /d "$(ProjectDir)OpenSSL-dll-$(PlatformName)\*.dll" "$(ProjectDir)$(OutDir)" + S7CommPlusDriver + S7CommPlusDriver + Copyright (c) 2025, DotNetProjects, Thomas Wiens + 0.1.0.0 + 0.1.0.0 + True + DotNetProjects.$(AssemblyName) + DotNetProjects.$(AssemblyName) + DotNetProjects + https://github.com/dotnetprojects/S7CommPlusDriver + LGPL-3.0-or-later + README.md - \ No newline at end of file + diff --git a/src/S7CommPlusDriver/S7CommPlusException.cs b/src/S7CommPlusDriver/S7CommPlusException.cs new file mode 100644 index 0000000..30ab724 --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusException.cs @@ -0,0 +1,72 @@ +using System; + +namespace S7CommPlusDriver +{ + public class S7CommPlusException : Exception + { + public S7CommPlusException(string operation, string endpoint, int errorCode, bool isTransient, string message, Exception innerException = null) + : base(message, innerException) + { + Operation = operation; + Endpoint = endpoint; + ErrorCode = errorCode; + IsTransient = isTransient; + } + + public string Operation { get; } + public string Endpoint { get; } + public int ErrorCode { get; } + public bool IsTransient { get; } + } + + public sealed class S7CommPlusConnectionException : S7CommPlusException + { + public S7CommPlusConnectionException(string operation, string endpoint, int errorCode, bool isTransient, string message, Exception innerException = null) + : base(operation, endpoint, errorCode, isTransient, message, innerException) + { + } + } + + public sealed class S7CommPlusTisWatchUnavailableException : S7CommPlusException + { + public S7CommPlusTisWatchUnavailableException(string operation, string endpoint, int errorCode, bool isTransient, string message, Exception innerException = null) + : base(operation, endpoint, errorCode, isTransient, message, innerException) + { + } + } + + public sealed class S7CommPlusTimeoutException : S7CommPlusException + { + public S7CommPlusTimeoutException(string operation, string endpoint, int errorCode, string message, Exception innerException = null) + : base(operation, endpoint, errorCode, true, message, innerException) + { + } + } + + public sealed class S7CommPlusLegitimationException : S7CommPlusException + { + public S7CommPlusLegitimationException(string operation, string endpoint, int errorCode, bool isTransient, string message, Exception innerException = null) + : base(operation, endpoint, errorCode, isTransient, message, innerException) + { + } + } + + public sealed class S7CommPlusWriteDisabledException : S7CommPlusException + { + public S7CommPlusWriteDisabledException(string endpoint) + : base("Write", endpoint, S7Consts.errCliAccessDenied, false, "Writes are disabled. Set S7CommPlusClientOptions.WriteEnabled to true to allow write operations.") + { + } + } + + public sealed class S7CommPlusUnsupportedSecurityModeException : S7CommPlusException + { + public S7CommPlusUnsupportedSecurityModeException(S7CommPlusSecurityMode securityMode, string endpoint, string message) + : base("Connect", endpoint, S7Consts.errCliFunctionNotImplemented, false, message) + { + SecurityMode = securityMode; + } + + public S7CommPlusSecurityMode SecurityMode { get; } + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusMetadataTypes.cs b/src/S7CommPlusDriver/S7CommPlusMetadataTypes.cs new file mode 100644 index 0000000..4d94aba --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusMetadataTypes.cs @@ -0,0 +1,110 @@ +using System; + +namespace S7CommPlusDriver +{ + public enum S7CommPlusBlockType + { + Unknown, + DB, + FB, + FC, + OB, + UDT, + } + + public enum S7CommPlusProgrammingLanguage : int + { + Undef = 0, + STL = 1, + LAD_CLASSIC = 2, + FBD_CLASSIC = 3, + SCL = 4, + DB = 5, + GRAPH = 6, + SDB = 7, + CPU_DB = 8, + CPU_SDB = 17, + CforS7 = 21, + HIGRAPH = 22, + CFC = 23, + SFC = 24, + S7_PDIAG = 26, + RSE = 29, + F_STL = 31, + F_LAD = 32, + F_FBD = 33, + F_DB = 34, + F_CALL = 35, + TechnoDB = 37, + F_LAD_LIB = 38, + F_FBD_LIB = 39, + ClassicEncryption = 41, + FCP = 50, + LAD_IEC = 100, + FBD_IEC = 101, + FLD = 102, + UDT = 150, + SDT = 151, + FBT = 152, + CB = 160, + ST = 161, + AX_CODE = 190, + AX_DATA = 191, + BMC_200 = 200, + Motion_DB = 201, + BMC_202 = 202, + BMC_203 = 203, + BMC_204 = 204, + BMC_205 = 205, + BMC_206 = 206, + BMC_207 = 207, + BMC_208 = 208, + BMC_209 = 209, + GRAPH_ACTIONS = 300, + GRAPH_SEQUENCE = 301, + GRAPH_ADDINFOS = 303, + GRAPH_PLUS = 310, + MC7plus = 400, + ProDiag = 500, + ProDiag_OB = 501, + CEM = 600, + } + + public sealed class S7CommPlusBlockInfo + { + public string Name { get; set; } + public uint Number { get; set; } + public S7CommPlusBlockType Type { get; set; } + public S7CommPlusProgrammingLanguage Language { get; set; } + public uint RelationId { get; set; } + public uint TypeInfoRelationId { get; set; } + } + + public sealed class S7CommPlusCpuInfo + { + public string PlcName { get; set; } + public string ProjectName { get; set; } + public Version VersionTia { get; set; } + public Version Version2 { get; set; } + public string CpuMlfb { get; set; } + public string CpuSerial { get; set; } + public Version CpuFirmware { get; set; } + } + + internal enum S7CommPlusBinaryArtifactType : uint + { + Undefined = 0u, + PlcFamily = 2147483649u, + PlcMc7plusData = 2147483650u, + PlcOptimizationInfoData = 2147483651u, + PlcClosedImmediateData = 2147483652u, + SimulatorFamily = 2147483665u, + SimulatorMc7plusData = 2147483666u, + SimulatorOptimizationInfoData = 2147483667u, + SimulatorClosedImmediateData = 2147483668u, + VirtualPlcFamilyKey = 2147483681u, + VirtualPlcMc7plusDataKey = 2147483682u, + VirtualPlcOptimizationInfoDataKey = 2147483683u, + VirtualPlcClosedImmediateDataKey = 2147483684u + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusNotificationReturnCode.cs b/src/S7CommPlusDriver/S7CommPlusNotificationReturnCode.cs new file mode 100644 index 0000000..0258d9b --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusNotificationReturnCode.cs @@ -0,0 +1,14 @@ +namespace S7CommPlusDriver +{ + public enum S7CommPlusNotificationReturnCode : byte + { + EndOfList = 0x00, + AddressingError = 0x03, + AddressingError1200 = 0x13, + AlarmObject = 0x81, + LegacyValue = 0x83, + ValueWithUInt32Reference = 0x92, + ValueWithVlqReference = 0x9B, + OnlineStatusTable = 0x9C + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusPlcStructureNode.cs b/src/S7CommPlusDriver/S7CommPlusPlcStructureNode.cs new file mode 100644 index 0000000..3c38a29 --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusPlcStructureNode.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; + +namespace S7CommPlusDriver +{ + public enum S7CommPlusPlcStructureNodeKind + { + Unit, + Folder, + Block, + Item + } + + public sealed class S7CommPlusPlcStructureNode + { + public S7CommPlusPlcStructureNode( + S7CommPlusPlcStructureNodeKind kind, + string name, + uint? relationId = null, + int? number = null, + string blockType = null, + string blockLanguage = null, + string subType = null, + DateTime? lastModified = null, + IReadOnlyList children = null) + { + Kind = kind; + Name = name ?? string.Empty; + RelationId = relationId; + Number = number; + BlockType = blockType; + BlockLanguage = blockLanguage; + SubType = subType; + LastModified = lastModified; + Children = children ?? Array.Empty(); + } + + public S7CommPlusPlcStructureNodeKind Kind { get; } + public string Name { get; } + public uint? RelationId { get; } + public int? Number { get; } + public string BlockType { get; } + public string BlockLanguage { get; } + public string SubType { get; } + public DateTime? LastModified { get; } + public IReadOnlyList Children { get; } + public bool IsBlock => Kind == S7CommPlusPlcStructureNodeKind.Block && RelationId.HasValue; + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusPlcStructureSnapshot.cs b/src/S7CommPlusDriver/S7CommPlusPlcStructureSnapshot.cs new file mode 100644 index 0000000..84bc25c --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusPlcStructureSnapshot.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; + +namespace S7CommPlusDriver +{ + public sealed class S7CommPlusProgramChangeMarker + { + public S7CommPlusProgramChangeMarker( + string structureHash, + DateTime? lastModified, + int blockCount, + int tagTableCount, + string hashAlgorithm = "SHA-256") + { + HashAlgorithm = string.IsNullOrWhiteSpace(hashAlgorithm) ? "SHA-256" : hashAlgorithm; + StructureHash = structureHash ?? string.Empty; + LastModified = lastModified; + BlockCount = blockCount; + TagTableCount = tagTableCount; + } + + public string HashAlgorithm { get; } + public string StructureHash { get; } + public DateTime? LastModified { get; } + public int BlockCount { get; } + public int TagTableCount { get; } + } + + public sealed class S7CommPlusPlcStructureSnapshot + { + public S7CommPlusPlcStructureSnapshot( + string xml, + S7CommPlusProgramChangeMarker programChangeMarker, + IReadOnlyList structure) + { + Xml = xml ?? string.Empty; + ProgramChangeMarker = programChangeMarker ?? throw new ArgumentNullException(nameof(programChangeMarker)); + Structure = structure ?? Array.Empty(); + } + + public string Xml { get; } + public S7CommPlusProgramChangeMarker ProgramChangeMarker { get; } + public IReadOnlyList Structure { get; } + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusProtocolSession.LegacyChallenge.cs b/src/S7CommPlusDriver/S7CommPlusProtocolSession.LegacyChallenge.cs new file mode 100644 index 0000000..d1c7d40 --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusProtocolSession.LegacyChallenge.cs @@ -0,0 +1,494 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using S7CommPlusDriver.Internal; +using Microsoft.Extensions.Logging; + +#if NET8_0_OR_GREATER +using HarpoS7; +using HarpoS7.Auth; +using HarpoS7.Integrity; +using HarpoS7.PublicKeys.Exceptions; +using HarpoS7.PublicKeys.Impl; +using HarpoS7.Utilities.Auth; +using HarpoS7.Utilities.Extensions; +#endif + +namespace S7CommPlusDriver +{ + internal partial class S7CommPlusProtocolSession + { + private const int LegacyDigestLength = LegacyOmsConstants.PacketDigestLength; + private const int LegacyDigestFieldLength = LegacyOmsConstants.PacketDigestFieldLength; + +#if NET8_0_OR_GREATER + private int ConnectLegacyChallenge(S7CommPlusClientOptions options) + { + var usesEngineeringTsap = string.Equals(options.RemoteTsap, LegacyOmsConstants.EngineeringTsap, StringComparison.Ordinal); + var res = ConnectLegacyChallengeCore(options, LegacyServerSessionRole.EngineeringSystem); + if (res == S7Consts.errS7CommPlusLegacyAuthentication) + { + options.Logger.LogDebug("Legacy S7CommPlus ES session role was rejected by PLC {Address}:{Port}; retrying HMI session role.", options.Address, options.Port); + res = ConnectLegacyChallengeCore(options, LegacyServerSessionRole.Hmi); + } + + if (res != 0 && !usesEngineeringTsap && string.Equals(options.RemoteTsap, LegacyOmsConstants.HmiTsap, StringComparison.Ordinal)) + { + var engineeringOptions = options.Clone(); + engineeringOptions.RemoteTsap = LegacyOmsConstants.EngineeringTsap; + options.Logger.LogDebug( + "Legacy S7CommPlus connection to PLC {Address}:{Port} failed with {ErrorCode}; retrying alternate TSAP {RemoteTsap}.", + options.Address, + options.Port, + res, + engineeringOptions.RemoteTsap); + res = ConnectLegacyChallengeCore(engineeringOptions, LegacyServerSessionRole.EngineeringSystem); + if (res == S7Consts.errS7CommPlusLegacyAuthentication) + { + options.Logger.LogDebug("Legacy S7CommPlus alternate ES session role was rejected by PLC {Address}:{Port}; retrying HMI session role on alternate TSAP.", options.Address, options.Port); + res = ConnectLegacyChallengeCore(engineeringOptions, LegacyServerSessionRole.Hmi); + } + } + + return res; + } + + private int ConnectLegacyChallengeCore(S7CommPlusClientOptions options, LegacyServerSessionRole serverSessionRole) + { + if (options.ConnectTimeoutMilliseconds > 0) + { + m_ReadTimeout = Math.Min( + options.RequestTimeoutMilliseconds, + Math.Max(1000, options.ConnectTimeoutMilliseconds - 1000)); + } + + var elapsed = Environment.TickCount; + PrepareClient(options.Address, options.RequestTimeoutMilliseconds, options.Port, options.LocalTsap, options.RemoteTsapBytes); + var res = m_client.Connect(); + if (res != 0) + { + return res; + } + options.Logger.LogDebug("Legacy S7CommPlus COTP connection established to PLC {Address}:{Port}.", options.Address, options.Port); + + res = SendLegacyCreateObjectRequest(serverSessionRole); + if (res != 0) + { + m_client.Disconnect(); + return res; + } + options.Logger.LogDebug("Legacy S7CommPlus CreateObject request sent to PLC {Address}:{Port}.", options.Address, options.Port); + + options.Logger.LogDebug("Legacy S7CommPlus CreateObject response received from PLC {Address}:{Port}.", options.Address, options.Port); + + res = m_client.SendEmptyDtData(); + if (res != 0) + { + m_client.Disconnect(); + return res; + } + + var createObjectPdu = m_ReceivedPDU.ToArray(); + var createObjRes = CreateObjectResponse.DeserializeFromPdu(m_ReceivedPDU); + if (createObjRes == null || createObjRes.ObjectIds == null || createObjRes.ObjectIds.Count == 0) + { + m_client.Disconnect(); + return S7Consts.errIsoInvalidPDU6; + } + + m_SessionId = createObjRes.ObjectIds[0]; + m_SessionId2 = createObjRes.ObjectIds.Count > 1 ? createObjRes.ObjectIds[1] : 0; + + if (!LegacyChallengeHandshake.TryParse(createObjectPdu, createObjRes, out var challenge)) + { + m_client.Disconnect(); + return S7Consts.errS7CommPlusLegacyAuthentication; + } + options.Logger.LogDebug( + "Legacy S7CommPlus CSI challenge received from PLC {Address}:{Port}: session {SessionId:X8}, Harpo family {KeyFamily}, OMS family {OmsKeyFamily}, fingerprint {Fingerprint}, auth frame {AuthenticationFrameKind}.", + options.Address, + options.Port, + challenge.SessionId, + challenge.KeyFamily, + challenge.OmsKeyFamily, + challenge.Fingerprint, + challenge.AuthenticationFrameKind); + + if (!TryCreateLegacyAuthentication(challenge, options, out var keyBlob, out var sessionKey, out var keyFamily, out var publicKey)) + { + m_client.Disconnect(); + return S7Consts.errS7CommPlusLegacyAuthentication; + } + + var serverSessionVersion = TryGetServerSession(createObjRes); + res = SendLegacyAuthenticationRequest(challenge.SessionId, challenge.AuthenticationFrameKind, keyFamily, keyBlob, sessionKey, publicKey, serverSessionVersion, serverSessionRole); + if (res != 0) + { + m_client.Disconnect(); + return res; + } + options.Logger.LogDebug("Legacy S7CommPlus authentication request sent to PLC {Address}:{Port}.", options.Address, options.Port); + + var setMultiVarRes = SetMultiVariablesResponse.DeserializeFromPdu(m_ReceivedPDU); + if (setMultiVarRes == null || setMultiVarRes.ReturnValue != 0) + { + m_client.Disconnect(); + return S7Consts.errS7CommPlusLegacyAuthentication; + } + options.Logger.LogDebug("Legacy S7CommPlus challenge authentication accepted by PLC {Address}:{Port}.", options.Address, options.Port); + + m_LegacySessionKey = sessionKey; + m_LegacyDigestActive = true; + m_SequenceNumber = 2; + m_IntegrityId = uint.MaxValue; + + options.Logger.LogDebug( + "Legacy S7CommPlus connection to PLC {Address}:{Port} is using conservative default communication limits.", + options.Address, + options.Port); + m_ReadTimeout = options.RequestTimeoutMilliseconds; + + var shouldLegitimate = !string.IsNullOrEmpty(options.Password) || !string.IsNullOrEmpty(options.Username); + var serverSession = shouldLegitimate ? TryGetServerSession(createObjRes) : null; + m_ServerSessionVersion = serverSessionVersion; + if (serverSession != null) + { + res = legitimate(serverSession, options.Password, options.Username); + if (res != 0) + { + m_client.Disconnect(); + return res; + } + } + else if (!string.IsNullOrEmpty(options.Password)) + { + m_client.Disconnect(); + return S7Consts.errCliFirmwareNotSupported; + } + + options.Logger.LogInformation("Legacy S7CommPlus challenge connection to PLC {Address}:{Port} established in {ElapsedMilliseconds} ms.", options.Address, options.Port, Environment.TickCount - elapsed); + return 0; + } + + private int SendLegacyCreateObjectRequest(LegacyServerSessionRole serverSessionRole) + { + var request = new CreateObjectRequest(ProtocolVersion.V1, 0, false); + request.SetTiaServerSessionData(serverSessionRole); + return SendS7plusFunctionObjectAndWait(request, m_ReadTimeout); + } + + private static ValueStruct TryGetServerSession(CreateObjectResponse createObjRes) + { + try + { + return createObjRes.ResponseObject.GetAttribute(Ids.ServerSessionVersion) as ValueStruct; + } + catch + { + return null; + } + } + + private bool TryCreateLegacyAuthentication( + LegacyChallengeHandshake challenge, + S7CommPlusClientOptions options, + out byte[] keyBlob, + out byte[] sessionKey, + out EPublicKeyFamily keyFamily, + out byte[] publicKey) + { + keyBlob = null; + sessionKey = null; + keyFamily = default; + publicKey = null; + + try + { + publicKey = options.LegacyPublicKeyResolver?.Invoke(challenge.Fingerprint); + if (publicKey == null || publicKey.Length == 0) + { + var store = new DefaultPublicKeyStore(); + publicKey = new byte[store.GetPublicKeyLength(challenge.Fingerprint)]; + store.ReadPublicKey(publicKey.AsSpan(), challenge.Fingerprint); + } + + keyFamily = challenge.KeyFamily; + sessionKey = new byte[Constants.SessionKeyLength]; + var blobLength = keyFamily == EPublicKeyFamily.PlcSim + ? CommonConstants.EncryptedBlobLengthPlcSim + : CommonConstants.EncryptedBlobLengthRealPlc; + keyBlob = new byte[blobLength]; + LegacyAuthenticationScheme.Authenticate( + keyBlob.AsSpan(), + sessionKey.AsSpan(), + challenge.Challenge.AsSpan(), + publicKey.AsSpan(), + keyFamily); + return true; + } + catch (UnknownPublicKeyException ex) + { + options.Logger.LogDebug(ex, "Legacy S7CommPlus public key {Fingerprint} is not available.", challenge.Fingerprint); + return false; + } + catch (Exception ex) + { + options.Logger.LogDebug(ex, "Legacy S7CommPlus authentication blob creation failed for PLC {Address}:{Port}.", options.Address, options.Port); + return false; + } + } + + private int SendLegacyAuthenticationRequest( + uint sessionId, + LegacyAuthenticationFrameKind authenticationFrameKind, + EPublicKeyFamily keyFamily, + byte[] keyBlob, + byte[] sessionKey, + byte[] publicKey, + ValueStruct serverSessionVersion, + LegacyServerSessionRole serverSessionRole) + { + Span publicKeyId = stackalloc byte[Constants.KeyIdLength]; + Span sessionKeyId = stackalloc byte[Constants.KeyIdLength]; + publicKey.AsSpan().DeriveKeyId(publicKeyId); + sessionKey.AsSpan().DeriveKeyId(sessionKeyId); + + var request = LegacyChallengeHandshake.CreateAuthenticationRequest( + keyFamily, + sessionId, + publicKeyId, + sessionKeyId, + keyBlob, + serverSessionVersion, + serverSessionRole, + authenticationFrameKind); + if (request == null) + { + return S7Consts.errS7CommPlusLegacyAuthentication; + } + + using var stream = new MemoryStream(); + request.Serialize(stream); + return SendRawS7plusPduAndWait(stream.ToArray(), (int)stream.Length, request.ProtocolVersion, m_ReadTimeout); + } + + private bool TryWriteLegacyDigest(byte[] destination, int digestOffset, byte[] data, int dataOffset, int dataLength) + { + if (!ShouldUseLegacyDigest(ProtocolVersion.V3)) + { + return false; + } + + HarpoPacketDigest.CalculateDigest( + destination.AsSpan(digestOffset + 1, LegacyDigestLength), + data.AsSpan(dataOffset, dataLength), + m_LegacySessionKey.AsSpan()); + destination[digestOffset] = LegacyDigestLength; + return true; + } + + private bool TryVerifyLegacyDigest( + byte[] digestBuffer, + int digestOffset, + byte[] data, + int dataOffset, + int dataLength) + { + if (!ShouldUseLegacyDigest(ProtocolVersion.V3)) + { + return false; + } + + if (digestBuffer[digestOffset] != LegacyDigestLength) + { + return false; + } + + Span expected = stackalloc byte[LegacyDigestLength]; + HarpoPacketDigest.CalculateDigest(expected, data.AsSpan(dataOffset, dataLength), m_LegacySessionKey.AsSpan()); + return expected.SequenceEqual(digestBuffer.AsSpan(digestOffset + 1, LegacyDigestLength)); + } + + private void TraceLegacyDigestReceive( + byte[] pdu, + int pduLength, + int digestOffset, + int bodyOffset, + int bodyLength, + int previousBodyLength, + byte[] accumulatedData, + int accumulatedDataOffset, + int accumulatedDataLength, + bool verified, + bool matched) + { + var traceDirectory = Environment.GetEnvironmentVariable("S7COMMPLUS_LEGACY_DIGEST_TRACE_DIR"); + if (string.IsNullOrWhiteSpace(traceDirectory) || !ShouldUseLegacyDigest(ProtocolVersion.V3)) + { + return; + } + + try + { + Directory.CreateDirectory(traceDirectory); + Span currentBodyDigest = stackalloc byte[LegacyDigestLength]; + Span accumulatedDigest = stackalloc byte[LegacyDigestLength]; + HarpoPacketDigest.CalculateDigest(currentBodyDigest, pdu.AsSpan(bodyOffset, bodyLength), m_LegacySessionKey.AsSpan()); + HarpoPacketDigest.CalculateDigest(accumulatedDigest, accumulatedData.AsSpan(accumulatedDataOffset, accumulatedDataLength), m_LegacySessionKey.AsSpan()); + + var receivedDigest = pdu.AsSpan(digestOffset + 1, LegacyDigestLength); + var includeRaw = string.Equals( + Environment.GetEnvironmentVariable("S7COMMPLUS_LEGACY_DIGEST_TRACE_INCLUDE_RAW"), + "1", + StringComparison.OrdinalIgnoreCase); + var sequence = Interlocked.Increment(ref m_LegacyDigestTraceSequence); + var path = Path.Combine(traceDirectory, $"legacy-digest-{Process.GetCurrentProcess().Id}.jsonl"); + var json = new StringBuilder(768); + json.Append('{'); + AppendJson(json, "sequence", sequence); + AppendJson(json, "pduLength", pduLength); + AppendJson(json, "bodyLength", bodyLength); + AppendJson(json, "previousBodyLength", previousBodyLength); + AppendJson(json, "accumulatedLength", accumulatedDataLength); + AppendJson(json, "verifiedByDriver", verified); + AppendJson(json, "matchedByDriver", matched); + AppendJson(json, "currentBodyMatches", receivedDigest.SequenceEqual(currentBodyDigest)); + AppendJson(json, "accumulatedBodyMatches", receivedDigest.SequenceEqual(accumulatedDigest)); + AppendJson(json, "receivedDigest", Convert.ToHexString(receivedDigest)); + AppendJson(json, "currentBodyDigest", Convert.ToHexString(currentBodyDigest)); + AppendJson(json, "accumulatedBodyDigest", Convert.ToHexString(accumulatedDigest)); + AppendJson(json, "bodySha256", Convert.ToHexString(SHA256.HashData(pdu.AsSpan(bodyOffset, bodyLength)))); + AppendJson(json, "accumulatedSha256", Convert.ToHexString(SHA256.HashData(accumulatedData.AsSpan(accumulatedDataOffset, accumulatedDataLength)))); + if (string.Equals( + Environment.GetEnvironmentVariable("S7COMMPLUS_LEGACY_DIGEST_TRACE_BRUTE_SUFFIX"), + "1", + StringComparison.OrdinalIgnoreCase)) + { + AppendJson( + json, + "currentSuffixMatchOffset", + FindLegacyDigestSuffixMatch( + ReadOnlySpan.Empty, + pdu.AsSpan(bodyOffset, bodyLength), + receivedDigest)); + AppendJson( + json, + "previousPlusSuffixMatchOffset", + previousBodyLength == 0 + ? -1 + : FindLegacyDigestSuffixMatch( + accumulatedData.AsSpan(accumulatedDataOffset, previousBodyLength), + pdu.AsSpan(bodyOffset, bodyLength), + receivedDigest)); + } + if (includeRaw) + { + AppendJson(json, "pduHex", Convert.ToHexString(pdu.AsSpan(0, pduLength))); + AppendJson(json, "bodyHex", Convert.ToHexString(pdu.AsSpan(bodyOffset, bodyLength))); + AppendJson(json, "accumulatedHex", Convert.ToHexString(accumulatedData.AsSpan(accumulatedDataOffset, accumulatedDataLength))); + } + + if (json[json.Length - 1] == ',') + { + json.Length--; + } + json.Append('}'); + json.AppendLine(); + File.AppendAllText(path, json.ToString()); + } + catch + { + // Diagnostics must never affect PLC communication. + } + } + + private int FindLegacyDigestSuffixMatch(ReadOnlySpan prefix, ReadOnlySpan body, ReadOnlySpan receivedDigest) + { + Span expected = stackalloc byte[LegacyDigestLength]; + if (prefix.Length == 0) + { + for (var offset = 0; offset <= body.Length; offset++) + { + HarpoPacketDigest.CalculateDigest(expected, body[offset..], m_LegacySessionKey.AsSpan()); + if (expected.SequenceEqual(receivedDigest)) + { + return offset; + } + } + + return -1; + } + + for (var offset = 0; offset <= body.Length; offset++) + { + var candidate = new byte[prefix.Length + body.Length - offset]; + prefix.CopyTo(candidate); + body[offset..].CopyTo(candidate.AsSpan(prefix.Length)); + HarpoPacketDigest.CalculateDigest(expected, candidate, m_LegacySessionKey.AsSpan()); + if (expected.SequenceEqual(receivedDigest)) + { + return offset; + } + } + + return -1; + } +#else + private int ConnectLegacyChallenge(S7CommPlusClientOptions options) + { + return S7Consts.errCliFunctionNotImplemented; + } + + private bool TryWriteLegacyDigest(byte[] destination, int digestOffset, byte[] data, int dataOffset, int dataLength) + { + return false; + } + + private bool TryVerifyLegacyDigest(byte[] digestBuffer, int digestOffset, byte[] data, int dataOffset, int dataLength) + { + return false; + } + + private void TraceLegacyDigestReceive( + byte[] pdu, + int pduLength, + int digestOffset, + int bodyOffset, + int bodyLength, + int previousBodyLength, + byte[] accumulatedData, + int accumulatedDataOffset, + int accumulatedDataLength, + bool verified, + bool matched) + { + } +#endif + + private static void AppendJson(StringBuilder builder, string name, long value) + { + builder.Append('"').Append(name).Append("\":").Append(value).Append(','); + } + + private static void AppendJson(StringBuilder builder, string name, bool value) + { + builder.Append('"').Append(name).Append("\":").Append(value ? "true" : "false").Append(','); + } + + private static void AppendJson(StringBuilder builder, string name, string value) + { + builder.Append('"').Append(name).Append("\":\"").Append(value).Append("\","); + } + + private bool ShouldUseLegacyDigest(byte protocolVersion) + { + return m_LegacyDigestActive + && protocolVersion == ProtocolVersion.V3 + && m_LegacySessionKey != null + && m_LegacySessionKey.Length >= 24; + } + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusProtocolSession.Session.cs b/src/S7CommPlusDriver/S7CommPlusProtocolSession.Session.cs new file mode 100644 index 0000000..f020d70 --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusProtocolSession.Session.cs @@ -0,0 +1,239 @@ +using S7CommPlusDriver.Alarming; +using S7CommPlusDriver.ClientApi; +using S7CommPlusDriver.Internal; +using System; +using System.Collections.Generic; +using System.IO; + +namespace S7CommPlusDriver +{ + internal partial class S7CommPlusProtocolSession : IS7CommPlusSession + { + private IS7CommPlusProtocolSession _protocolSession; + private S7CommPlusTagSubscriptionService _tagSubscriptions; + private S7CommPlusAlarmSubscriptionService _alarmSubscriptions; + private S7CommPlusTisWatchSubscriptionService _tisWatchSubscriptions; + private S7CommPlusAlarmBrowseService _alarmBrowser; + private S7CommPlusMetadataService _metadata; + private S7CommPlusTextListService _textLists; + + private IS7CommPlusProtocolSession ProtocolSession => _protocolSession ??= new ProtocolSessionAdapter(this); + private S7CommPlusTagSubscriptionService TagSubscriptions => _tagSubscriptions ??= new S7CommPlusTagSubscriptionService(ProtocolSession); + private S7CommPlusAlarmSubscriptionService AlarmSubscriptions => _alarmSubscriptions ??= new S7CommPlusAlarmSubscriptionService(ProtocolSession); + private S7CommPlusTisWatchSubscriptionService TisWatchSubscriptions => _tisWatchSubscriptions ??= new S7CommPlusTisWatchSubscriptionService(ProtocolSession); + private S7CommPlusAlarmBrowseService AlarmBrowser => _alarmBrowser ??= new S7CommPlusAlarmBrowseService(ProtocolSession); + private S7CommPlusMetadataService Metadata => _metadata ??= new S7CommPlusMetadataService(ProtocolSession); + private S7CommPlusTextListService TextLists => _textLists ??= new S7CommPlusTextListService(ProtocolSession, Metadata); + + int IS7CommPlusSession.Connect(S7CommPlusClientOptions options) + { + return Connect(options); + } + + string IS7CommPlusSession.LastErrorDetail => m_LastErrorDetail; + + int IS7CommPlusSession.Disconnect(int timeoutMilliseconds) + { + return TryDisconnect(timeoutMilliseconds); + } + + int IS7CommPlusSession.CloseTransport(int timeoutMilliseconds) + { + return CloseTransport(timeoutMilliseconds); + } + + int IS7CommPlusSession.Legitimate(string password, string username) + { + return Legitimate(password, username); + } + + int IS7CommPlusSession.BrowseVariables(out List variables) + { + return Browse(out variables); + } + + int IS7CommPlusSession.BrowseBlocks(out List blocks) + { + return Metadata.BrowseBlocks(out blocks); + } + + int IS7CommPlusSession.GetPlcStructureXml(out S7CommPlusPlcStructureSnapshot plcStructure) + { + return Metadata.GetPlcStructureXml(out plcStructure); + } + + int IS7CommPlusSession.GetBlockContent(uint relationId, out S7CommPlusClientBlockContent blockContent) + { + return Metadata.GetBlockContent(relationId, out blockContent); + } + + PlcTag IS7CommPlusSession.GetPlcTagBySymbol(string symbol) + { + return getPlcTagBySymbol(symbol); + } + + int IS7CommPlusSession.GetCpuInfo(out S7CommPlusCpuInfo cpuInfo) + { + return Metadata.GetCpuInfo(out cpuInfo); + } + + int IS7CommPlusSession.GetCpuState(out S7CommPlusCpuState cpuState) + { + return Metadata.GetCpuState(out cpuState); + } + + int IS7CommPlusSession.GetCpuCycleTime(out S7CommPlusCpuCycleTime cycleTime) + { + return Metadata.GetCpuCycleTime(out cycleTime); + } + + int IS7CommPlusSession.GetCpuMemoryUsage(out S7CommPlusCpuMemoryUsage memoryUsage) + { + return Metadata.GetCpuMemoryUsage(out memoryUsage); + } + + int IS7CommPlusSession.SetCpuOperatingState(int operatingStateRequest) + { + return SetPlcOperatingState(operatingStateRequest); + } + + int IS7CommPlusSession.GetCpuCultureInfo(out S7CommPlusCpuCultureInfo cultureInfo) + { + return Metadata.GetCpuCultureInfo(out cultureInfo); + } + + int IS7CommPlusSession.GetTextLists(IEnumerable languageIds, out S7CommPlusTextListCatalog textLists) + { + return TextLists.GetTextLists(languageIds, out textLists); + } + + int IS7CommPlusSession.GetCommunicationResources(out S7CommPlusCommunicationResourceSnapshot resources) + { + return GetCommunicationResources(out resources); + } + + int IS7CommPlusSession.GetActiveAlarms(out List alarmList, int languageId, Func textListResolver) + { + return AlarmBrowser.GetActiveAlarms(out alarmList, languageId, textListResolver); + } + + int IS7CommPlusSession.ReadValues(List addresses, out List values, out List errors) + { + return ReadValues(addresses, out values, out errors); + } + + int IS7CommPlusSession.WriteValues(List addresses, List values, out List errors) + { + return WriteValues(addresses, values, out errors); + } + + int IS7CommPlusSession.CreateTagSubscription(List tags, ushort cycleTimeMilliseconds, short initialCreditLimit, out uint subscriptionObjectId) + { + return TagSubscriptions.Create(tags, cycleTimeMilliseconds, initialCreditLimit, out subscriptionObjectId); + } + + int IS7CommPlusSession.WaitForTagSubscriptionNotifications(uint subscriptionObjectId, int timeoutMilliseconds, short creditLimitStep, out List notifications) + { + return TagSubscriptions.WaitForNotifications(subscriptionObjectId, timeoutMilliseconds, creditLimitStep, out notifications); + } + + int IS7CommPlusSession.DeleteTagSubscription(uint subscriptionObjectId) + { + return TagSubscriptions.Delete(subscriptionObjectId); + } + + int IS7CommPlusSession.CreateAlarmSubscription(uint[] languageIds, short initialCreditLimit, out uint subscriptionObjectId) + { + return AlarmSubscriptions.Create(languageIds, initialCreditLimit, out subscriptionObjectId); + } + + int IS7CommPlusSession.WaitForAlarmNotifications(uint subscriptionObjectId, int timeoutMilliseconds, short creditLimitStep, out List notifications) + { + return AlarmSubscriptions.WaitForNotifications(subscriptionObjectId, timeoutMilliseconds, creditLimitStep, out notifications); + } + + int IS7CommPlusSession.DeleteAlarmSubscription(uint subscriptionObjectId) + { + return AlarmSubscriptions.Delete(subscriptionObjectId); + } + + int IS7CommPlusSession.CreateTisWatchSubscription(S7CommPlusTisWatchRequest request, out uint subscriptionObjectId) + { + return TisWatchSubscriptions.Create(request, out subscriptionObjectId); + } + + int IS7CommPlusSession.WaitForTisWatchNotifications(uint subscriptionObjectId, int timeoutMilliseconds, out List notifications) + { + return TisWatchSubscriptions.WaitForNotifications(subscriptionObjectId, timeoutMilliseconds, out notifications); + } + + string IS7CommPlusSession.LastTisWatchDiagnostic => TisWatchSubscriptions.LastDiagnostic; + + int IS7CommPlusSession.DeleteTisWatchSubscription(uint subscriptionObjectId) + { + return TisWatchSubscriptions.Delete(subscriptionObjectId); + } + + private sealed class ProtocolSessionAdapter : IS7CommPlusProtocolSession + { + private readonly S7CommPlusProtocolSession _connection; + + public ProtocolSessionAdapter(S7CommPlusProtocolSession connection) + { + _connection = connection; + } + + public int LastError + { + get => _connection.m_LastError; + set => _connection.m_LastError = value; + } + + public int ReadTimeout => _connection.m_ReadTimeout; + public uint SessionId => _connection.m_SessionId; + public uint SessionId2 => _connection.m_SessionId2; + public MemoryStream ReceivedPdu => _connection.m_ReceivedPDU; + + public int SendFunction(IS7pRequest request) + { + return _connection.SendS7plusFunctionObjectSerialized(request); + } + + public int SendFunctionAndWait(IS7pRequest request) + { + return _connection.SendS7plusFunctionObjectAndWait(request, _connection.m_ReadTimeout); + } + + public void WaitForPdu(int timeoutMilliseconds) + { + _connection.WaitForNewS7plusReceived(timeoutMilliseconds); + } + + public int WaitForNotification(uint subscriptionObjectId, int timeoutMilliseconds, out Notification notification) + { + return _connection.WaitForNotification(subscriptionObjectId, timeoutMilliseconds, out notification); + } + + public int CheckResponse(IS7pRequest request, IS7pResponse response) + { + return _connection.checkResponseWithIntegrity(request, response); + } + + public int DeleteObject(uint objectId) + { + return _connection.DeleteObject(objectId); + } + + public void DisconnectTransport() + { + _connection.m_client?.Disconnect(); + } + + public void ClearSessionIds() + { + _connection.m_SessionId = 0; + _connection.m_SessionId2 = 0; + } + } + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusConnection.cs b/src/S7CommPlusDriver/S7CommPlusProtocolSession.cs similarity index 56% rename from src/S7CommPlusDriver/S7CommPlusConnection.cs rename to src/S7CommPlusDriver/S7CommPlusProtocolSession.cs index 8a16b7e..0b36d5a 100644 --- a/src/S7CommPlusDriver/S7CommPlusConnection.cs +++ b/src/S7CommPlusDriver/S7CommPlusProtocolSession.cs @@ -1,7 +1,7 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver - * + * * Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de * * This file is part of S7CommPlusDriver. @@ -17,26 +17,32 @@ using System.Collections.Generic; using System.Text; using System.Threading; +using System.Threading.Channels; using System.IO; using System.Linq; using System.Diagnostics; using S7CommPlusDriver.ClientApi; +using S7CommPlusDriver.Internal; using System.Text.RegularExpressions; -using System.Security.Cryptography; +using Microsoft.Extensions.Logging; namespace S7CommPlusDriver { - public partial class S7CommPlusConnection + internal partial class S7CommPlusProtocolSession { #region Private Members private S7Client m_client; private MemoryStream m_ReceivedPDU; private MemoryStream m_ReceivedTempPDU; - private Queue m_ReceivedPDUs = new Queue(); - private Mutex m_Mutex = new Mutex(); + private Channel m_ReceivedPDUs = CreateReceiveChannel(); + private readonly object m_RequestLock = new object(); + private readonly object m_ReceiveDispatchLock = new object(); + private readonly object m_NotificationQueueLock = new object(); + private readonly Dictionary> m_NotificationQueues = new Dictionary>(); private bool m_ReceivedNeedMoreDataForCompletePDU; private bool m_NewS7CommPlusReceived; + private IS7pRequest m_LastSentRequestForWait; private UInt32 m_SessionId; private UInt32 m_SessionId2; public UInt32 SessionId2 @@ -49,7 +55,15 @@ public UInt32 SessionId2 private UInt16 m_SequenceNumber = 0; private UInt32 m_IntegrityId = 0; private UInt32 m_IntegrityId_Set = 0; - private CommRessources m_CommRessources = new CommRessources(); + private bool m_LegacyDigestActive; + private byte[] m_LegacySessionKey; +#if NET8_0_OR_GREATER + private long m_LegacyDigestTraceSequence; +#endif + private ValueStruct m_ServerSessionVersion; + private S7CommPlusSecurityMode m_NegotiatedSecurityMode = S7CommPlusSecurityMode.Tls; + private S7CommPlusCommunicationResourceSnapshot m_CommunicationResources = new S7CommPlusCommunicationResourceSnapshot(); + private string m_LastErrorDetail = string.Empty; private List dbInfoList; private List typeInfoList = new List(); @@ -61,6 +75,27 @@ public UInt32 SessionId2 #endregion #region Private Methods + private sealed class ReceivedS7PlusPdu + { + public ReceivedS7PlusPdu(MemoryStream pdu, int errorCode = 0) + { + Pdu = pdu; + ErrorCode = errorCode; + } + + public MemoryStream Pdu { get; } + public int ErrorCode { get; } + } + + private static Channel CreateReceiveChannel() + { + return Channel.CreateUnbounded(new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = false, + AllowSynchronousContinuations = false + }); + } private UInt16 GetNextSequenceNumber() { @@ -115,40 +150,47 @@ private UInt32 GetNextIntegrityId(ushort functioncode) private void WaitForNewS7plusReceived(int Timeout) { - bool Expired = false; - int Elapsed = Environment.TickCount; - bool done = false; - - m_Mutex.WaitOne(); - if (m_ReceivedPDUs.Count > 0) + var expectedRequest = m_LastSentRequestForWait; + m_LastError = expectedRequest == null + ? ReceiveNextS7plusPdu(Timeout, out m_ReceivedPDU) + : WaitForExpectedResponse(expectedRequest, Timeout); + if (m_LastError != 0) { - m_ReceivedPDU = m_ReceivedPDUs.Dequeue(); - done = true; + Trace.WriteLine("S7CommPlusProtocolSession - WaitForNewS7plusReceived: ERROR: " + S7Client.ErrorText(m_LastError)); } - m_Mutex.ReleaseMutex(); + } - while (!done && !Expired) + private int ReceiveNextS7plusPdu(int timeout, out MemoryStream pdu) + { + pdu = null; + try { - Thread.Sleep(2); - Expired = Environment.TickCount - Elapsed > Timeout; - m_Mutex.WaitOne(); - if (m_ReceivedPDUs.Count > 0) + using var cts = new CancellationTokenSource(Math.Max(1, timeout)); + var received = m_ReceivedPDUs.Reader.ReadAsync(cts.Token).AsTask().GetAwaiter().GetResult(); + if (received.ErrorCode != 0) { - m_ReceivedPDU = m_ReceivedPDUs.Dequeue(); - done = true; + return received.ErrorCode; } - m_Mutex.ReleaseMutex(); + pdu = received.Pdu; + return 0; } - - if (Expired) + catch (OperationCanceledException) + { + return S7Consts.errCliJobTimeout; + } + catch (ChannelClosedException) { - Console.WriteLine("S7CommPlusConnection - WaitForNewS7plusReceived: ERROR: Timeout!"); - m_LastError = S7Consts.errTCPDataReceive; + return S7Consts.errTCPNotConnected; } } private int SendS7plusFunctionObject(IS7pRequest funcObj) { + if (m_LegacyDigestActive && funcObj.ProtocolVersion != ProtocolVersion.V1) + { + funcObj.ProtocolVersion = ProtocolVersion.V3; + } + // If we don't have a SessionId, this must be the first CreateObjectRequest, where we use the Id for NullServerSession if (m_SessionId == 0) { @@ -165,12 +207,328 @@ private int SendS7plusFunctionObject(IS7pRequest funcObj) { funcObj.IntegrityId = GetNextIntegrityId(funcObj.FunctionCode); } + m_LastSentRequestForWait = funcObj; MemoryStream stream = new MemoryStream(); funcObj.Serialize(stream); return SendS7plusPDUdata(stream.ToArray(), (int)stream.Length, funcObj.ProtocolVersion); } + private int SendS7plusFunctionObjectSerialized(IS7pRequest funcObj) + { + lock (m_RequestLock) + { + try + { + return SendS7plusFunctionObject(funcObj); + } + finally + { + if (ReferenceEquals(m_LastSentRequestForWait, funcObj)) + { + m_LastSentRequestForWait = null; + } + } + } + } + + private int SendS7plusFunctionObjectAndWait(IS7pRequest funcObj, int timeout) + { + lock (m_RequestLock) + { + try + { + var result = SendS7plusFunctionObject(funcObj); + if (result != 0) + { + return result; + } + + m_LastError = 0; + return WaitForExpectedResponse(funcObj, timeout); + } + finally + { + if (ReferenceEquals(m_LastSentRequestForWait, funcObj)) + { + m_LastSentRequestForWait = null; + } + } + } + } + + private int SendRawS7plusPduAndWait(byte[] sendPduData, int bytesToSend, byte protoVersion, int timeout) + { + lock (m_RequestLock) + { + var result = SendS7plusPDUdata(sendPduData, bytesToSend, protoVersion); + if (result != 0) + { + return result; + } + + m_LastSentRequestForWait = null; + m_LastError = ReceiveNextS7plusPdu(timeout, out m_ReceivedPDU); + if (m_LastError != 0) + { + Trace.WriteLine("S7CommPlusProtocolSession - SendRawS7plusPduAndWait: ERROR: " + S7Client.ErrorText(m_LastError)); + } + return m_LastError; + } + } + + private int WaitForExpectedResponse(IS7pRequest request, int timeout) + { + var deadline = Environment.TickCount64 + Math.Max(1, timeout); + while (Environment.TickCount64 < deadline) + { + var remaining = (int)Math.Min(50, Math.Max(1, deadline - Environment.TickCount64)); + var result = DispatchOneReceivedPdu(remaining, request, 0, out var responsePdu, out _); + if (result == S7Consts.errCliJobTimeout || result == S7Consts.errTCPReceiveTimeout) + { + continue; + } + if (result != 0) + { + return result; + } + if (responsePdu != null) + { + m_ReceivedPDU = responsePdu; + return 0; + } + } + + return S7Consts.errCliJobTimeout; + } + + private int WaitForNotification(uint subscriptionObjectId, int timeout, out Notification notification) + { + notification = null; + var deadline = Environment.TickCount64 + Math.Max(1, timeout); + while (Environment.TickCount64 < deadline) + { + if (TryDequeueNotification(subscriptionObjectId, out notification)) + { + return 0; + } + + var remaining = (int)Math.Min(50, Math.Max(1, deadline - Environment.TickCount64)); + var result = DispatchOneReceivedPdu(remaining, null, subscriptionObjectId, out _, out notification); + if (result == S7Consts.errCliJobTimeout || result == S7Consts.errTCPReceiveTimeout) + { + continue; + } + if (result != 0) + { + return result; + } + if (notification != null && (subscriptionObjectId == 0 || NotificationMatches(notification, subscriptionObjectId))) + { + return 0; + } + } + + notification = null; + return S7Consts.errCliJobTimeout; + } + + private int DispatchOneReceivedPdu(int timeout, IS7pRequest expectedResponse, uint notificationSubscriptionObjectId, out MemoryStream responsePdu, out Notification matchingNotification) + { + responsePdu = null; + matchingNotification = null; + + if (!Monitor.TryEnter(m_ReceiveDispatchLock, timeout)) + { + return S7Consts.errCliJobTimeout; + } + + try + { + var result = ReceiveNextS7plusPdu(timeout, out var pdu); + if (result != 0) + { + return result; + } + + if (TryPeekS7PlusPdu(pdu, out var opcode, out var function, out var sequenceNumber)) + { + if (opcode == Opcode.Response) + { + if (expectedResponse != null + && function == expectedResponse.FunctionCode + && sequenceNumber == expectedResponse.SequenceNumber) + { + pdu.Position = 0; + responsePdu = pdu; + return 0; + } + + Trace.WriteLine($"S7CommPlusProtocolSession - Dispatch: discarded unexpected response function=0x{function:X4} seq={sequenceNumber}."); + return 0; + } + + if (opcode == Opcode.Notification) + { + pdu.Position = 0; + var notification = Notification.DeserializeFromPdu(pdu); + if (notification == null) + { + return S7Consts.errIsoInvalidPDU; + } + + if (expectedResponse == null + && (notificationSubscriptionObjectId == 0 || NotificationMatches(notification, notificationSubscriptionObjectId))) + { + if (notificationSubscriptionObjectId != 0) + { + EnqueueNotification(notification, notificationSubscriptionObjectId); + } + matchingNotification = notification; + return 0; + } + + EnqueueNotification(notification); + return 0; + } + } + + return 0; + } + finally + { + Monitor.Exit(m_ReceiveDispatchLock); + } + } + + private static bool TryPeekS7PlusPdu(MemoryStream pdu, out byte opcode, out ushort function, out ushort sequenceNumber) + { + opcode = 0; + function = 0; + sequenceNumber = 0; + if (pdu == null) + { + return false; + } + + var position = pdu.Position; + try + { + pdu.Position = 0; + if (pdu.Length < 2) + { + return false; + } + + S7p.DecodeByte(pdu, out var protocolVersion); + if (protocolVersion == ProtocolVersion.SystemEvent) + { + return false; + } + + S7p.DecodeByte(pdu, out opcode); + if (opcode == Opcode.Response) + { + if (pdu.Length < 10) + { + return false; + } + S7p.DecodeUInt16(pdu, out _); + S7p.DecodeUInt16(pdu, out function); + S7p.DecodeUInt16(pdu, out _); + S7p.DecodeUInt16(pdu, out sequenceNumber); + } + + return opcode == Opcode.Response || opcode == Opcode.Notification; + } + finally + { + pdu.Position = position; + } + } + + private void EnqueueNotification(Notification notification) + { + EnqueueNotification(notification, 0); + } + + private void EnqueueNotification(Notification notification, uint excludedSubscriptionObjectId) + { + lock (m_NotificationQueueLock) + { + if (notification.SubscriptionObjectId != excludedSubscriptionObjectId) + { + EnqueueNotification(notification.SubscriptionObjectId, notification); + } + if (notification.P2SubscriptionObjectId != 0 && notification.P2SubscriptionObjectId != notification.SubscriptionObjectId) + { + if (notification.P2SubscriptionObjectId != excludedSubscriptionObjectId) + { + EnqueueNotification(notification.P2SubscriptionObjectId, notification); + } + } + Monitor.PulseAll(m_NotificationQueueLock); + } + } + + private void EnqueueNotification(uint subscriptionObjectId, Notification notification) + { + if (subscriptionObjectId == 0) + { + return; + } + + if (!m_NotificationQueues.TryGetValue(subscriptionObjectId, out var queue)) + { + queue = new Queue(); + m_NotificationQueues.Add(subscriptionObjectId, queue); + } + queue.Enqueue(notification); + } + + private bool TryDequeueNotification(uint subscriptionObjectId, out Notification notification) + { + lock (m_NotificationQueueLock) + { + if (subscriptionObjectId != 0) + { + if (m_NotificationQueues.TryGetValue(subscriptionObjectId, out var queue) && queue.Count > 0) + { + notification = queue.Dequeue(); + return true; + } + } + else + { + foreach (var queue in m_NotificationQueues.Values) + { + if (queue.Count > 0) + { + notification = queue.Dequeue(); + return true; + } + } + } + } + + notification = null; + return false; + } + + private static bool NotificationMatches(Notification notification, uint subscriptionObjectId) + { + return notification.SubscriptionObjectId == subscriptionObjectId + || notification.P2SubscriptionObjectId == subscriptionObjectId; + } + + private void ClearNotificationQueues() + { + lock (m_NotificationQueueLock) + { + m_NotificationQueues.Clear(); + } + } + private int SendS7plusPDUdata(byte[] sendPduData, int bytesToSend, byte protoVersion) { m_LastError = 0; @@ -178,15 +536,18 @@ private int SendS7plusPDUdata(byte[] sendPduData, int bytesToSend, byte protoVer int curSize; int sourcePos = 0; int sendLen; - int NegotiatedIsoPduSize = 1024;// TODO: Respect the negotiated TPDU size - - // 4 Byte TPKT Header - // 3 Byte ISO-Header - // 5 Byte TLS Header + 17 Bytes addition from TLS - // 4 Byte S7CommPlus Header - // 4 Byte S7CommPlus Trailer (must fit into last PDU) - int MaxSize = NegotiatedIsoPduSize - 4 - 3 - 5 - 17 - 4 - 4; - byte[] packet = new byte[MaxSize + 4]; //max packet size is always MaxSize + PDU Header + bool useLegacyDigest = ShouldUseLegacyDigest(protoVersion); + int legacyDigestLength = useLegacyDigest ? LegacyDigestFieldLength : 0; + int MaxSize = GetMaxS7CommPlusPayloadSize(legacyDigestLength); + if (MaxSize <= 0) + { + return S7Consts.errIsoInvalidPDU; + } + if (useLegacyDigest && bytesToSend > MaxSize) + { + return S7Consts.errS7CommPlusLegacyRequestTooLarge; + } + byte[] packet = new byte[MaxSize + S7CommPlusProtocolConstants.S7CommPlusHeaderLength + legacyDigestLength]; while (bytesToSend > 0) { @@ -201,20 +562,32 @@ private int SendS7plusPDUdata(byte[] sendPduData, int bytesToSend, byte protoVer bytesToSend -= curSize; } // Header - packet[0] = 0x72; + packet[0] = S7CommPlusProtocolConstants.FrameMarker; packet[1] = protoVersion; - packet[2] = (byte)(curSize >> 8); - packet[3] = (byte)(curSize & 0x00FF); + int s7DataLength = curSize + legacyDigestLength; + packet[2] = (byte)(s7DataLength >> 8); + packet[3] = (byte)(s7DataLength & 0x00FF); // Data part - Array.Copy(sendPduData, sourcePos, packet, 4, curSize); + if (useLegacyDigest) + { + if (!TryWriteLegacyDigest(packet, 4, sendPduData, 0, sourcePos + curSize)) + { + return S7Consts.errS7CommPlusDigestMismatch; + } + Array.Copy(sendPduData, sourcePos, packet, 4 + legacyDigestLength, curSize); + } + else + { + Array.Copy(sendPduData, sourcePos, packet, 4, curSize); + } sourcePos += curSize; - sendLen = 4 + curSize; + sendLen = S7CommPlusProtocolConstants.S7CommPlusHeaderLength + s7DataLength; // Trailer only in last packet if (bytesToSend == 0) { - Array.Resize(ref packet, sendLen + 4); //resize only the last package to sendLen + TrailerLen - packet[sendLen] = 0x72; + Array.Resize(ref packet, sendLen + S7CommPlusProtocolConstants.S7CommPlusTrailerLength); + packet[sendLen] = S7CommPlusProtocolConstants.FrameMarker; sendLen++; packet[sendLen] = protoVersion; sendLen++; @@ -224,10 +597,56 @@ private int SendS7plusPDUdata(byte[] sendPduData, int bytesToSend, byte protoVer sendLen++; } m_client.Send(packet); + if (m_client._LastError != 0) + { + return m_client._LastError; + } } return m_LastError; } + private static int GetMaxS7CommPlusPayloadSize(int legacyDigestLength) + { + return S7CommPlusProtocolConstants.DefaultIsoTpduSize + - S7CommPlusProtocolConstants.TpktHeaderLength + - S7CommPlusProtocolConstants.CotpHeaderLength + - S7CommPlusProtocolConstants.TlsRecordHeaderLength + - S7CommPlusProtocolConstants.TlsAesGcmRecordOverhead + - legacyDigestLength + - S7CommPlusProtocolConstants.S7CommPlusHeaderLength + - S7CommPlusProtocolConstants.S7CommPlusTrailerLength; + } + + private int GetLegacySingleFramePayloadLimit(byte protocolVersion) + { + if (!ShouldUseLegacyDigest(protocolVersion)) + { + return int.MaxValue; + } + + return GetMaxS7CommPlusPayloadSize(LegacyDigestFieldLength); + } + + private bool ExceedsLegacySingleFramePayload(GetMultiVariablesRequest request, int maxPayloadSize) + { + if (maxPayloadSize == int.MaxValue) + { + return false; + } + + var probe = new GetMultiVariablesRequest(ProtocolVersion.V3) + { + SessionId = UInt32.MaxValue, + SequenceNumber = UInt16.MaxValue, + IntegrityId = UInt32.MaxValue + }; + probe.AddressList.AddRange(request.AddressList); + + using var stream = new MemoryStream(); + probe.Serialize(stream); + return stream.Length > maxPayloadSize; + } + private void OnDataReceived(byte[] PDU, int len) { // In this method, we've got always a complete TPDU (from protocol layer above) without fragmentation @@ -235,7 +654,7 @@ private void OnDataReceived(byte[] PDU, int len) // If not fragmented, then TPKT.Length - 15 is equal of the length in S7CommPlus.Header. // 15 bytes because: 4 Bytes TPKT.Header.len + 3 Bytes ISO.Header.Len + 4 Bytes S7CommPlus.Header.len + 4 Bytes S7CommPlus.trailer.Len. // Since the pure userdata of the TPDU comes in here, that is only minus 4 bytes header + 4 bytes trailer. - // + // // Special handling for SystemEvents with ProtocolVersion = 0xfe: // Here's only a header. // Because of this, the first byte for the ProtocolVersion must be written in then stream at first. @@ -245,7 +664,7 @@ private void OnDataReceived(byte[] PDU, int len) // This method is called from a different thread. // If we use subscriptions or alarming, we may get new data before the last PDU was processed completely. // First step we push the complete PDU to a queue. - // TODO: m_LastError handling would also not work as expected. This needs some more redesign. + // Receive errors are published through the receive queue so request waiters fail immediately. if (!m_ReceivedNeedMoreDataForCompletePDU) { @@ -256,10 +675,10 @@ private void OnDataReceived(byte[] PDU, int len) int pos = 0; int s7HeaderDataLen = 0; // Check header - if (PDU[pos] != 0x72) + if (PDU[pos] != S7CommPlusProtocolConstants.FrameMarker) { m_ReceivedNeedMoreDataForCompletePDU = false; - m_LastError = S7Consts.errIsoInvalidPDU; + PublishReceiveError(S7Consts.errIsoInvalidPDU1); return; } pos++; @@ -267,7 +686,7 @@ private void OnDataReceived(byte[] PDU, int len) if (protoVersion != ProtocolVersion.V1 && protoVersion != ProtocolVersion.V2 && protoVersion != ProtocolVersion.V3 && protoVersion != ProtocolVersion.SystemEvent) { m_ReceivedNeedMoreDataForCompletePDU = false; - m_LastError = S7Consts.errIsoInvalidPDU; + PublishReceiveError(S7Consts.errIsoInvalidPDU2); return; } // For the first fragment, write the ProtocolVersion into the stream in advance @@ -288,7 +707,7 @@ private void OnDataReceived(byte[] PDU, int len) // As we don't have a trailer on this types, it's not possible that they are transmitted as fragments. if (protoVersion == ProtocolVersion.SystemEvent) { - Console.WriteLine("S7CommPlusConnection - OnDataReceived: ProtocolVersion 0xfe SystemEvent received"); + Trace.WriteLine("S7CommPlusProtocolSession - OnDataReceived: ProtocolVersion 0xfe SystemEvent received"); m_ReceivedTempPDU.Write(PDU, pos, s7HeaderDataLen); pos += s7HeaderDataLen; // Create SystemEventObject @@ -299,19 +718,68 @@ private void OnDataReceived(byte[] PDU, int len) var sysevt = SystemEvent.DeserializeFromPdu(m_ReceivedTempPDU); if (sysevt.IsFatalError()) { - Console.WriteLine("S7CommPlusConnection - OnDataReceived: SystemEvent has fatal error"); + Trace.WriteLine("S7CommPlusProtocolSession - OnDataReceived: SystemEvent has fatal error"); // Termination neccessary - m_LastError = S7Consts.errIsoInvalidPDU; + PublishReceiveError(S7Consts.errIsoInvalidPDU3); } else { - Console.WriteLine("S7CommPlusConnection - OnDataReceived: SystemEvent with non fatal error, do nothing"); + Trace.WriteLine("S7CommPlusProtocolSession - OnDataReceived: SystemEvent with non fatal error, do nothing"); } } else { + var dataPos = pos; + var dataLen = s7HeaderDataLen; + if (ShouldUseLegacyDigest(protoVersion)) + { + if (dataLen < LegacyDigestFieldLength) + { + m_ReceivedNeedMoreDataForCompletePDU = false; + PublishReceiveError(S7Consts.errS7CommPlusDigestMismatch); + return; + } + var legacyBodyPos = dataPos + LegacyDigestFieldLength; + var legacyBodyLen = dataLen - LegacyDigestFieldLength; + var previousBodyLen = Math.Max(0, (int)m_ReceivedTempPDU.Length - 1); + var digestData = previousBodyLen == 0 + ? PDU + : BuildLegacyAccumulatedDigestData(PDU, legacyBodyPos, legacyBodyLen, previousBodyLen); + var digestDataOffset = previousBodyLen == 0 ? legacyBodyPos : 0; + var digestDataLen = previousBodyLen + legacyBodyLen; + // Siemens OMS verifies the first protected large frame, then sets an internal + // "MAC already verified" flag for continuation fragments in the same logical PDU. + var legacyDigestVerified = previousBodyLen == 0; + var legacyDigestMatched = !legacyDigestVerified || TryVerifyLegacyDigest( + PDU, + dataPos, + digestData, + digestDataOffset, + digestDataLen); + TraceLegacyDigestReceive( + PDU, + len, + dataPos, + legacyBodyPos, + legacyBodyLen, + previousBodyLen, + digestData, + digestDataOffset, + digestDataLen, + legacyDigestVerified, + legacyDigestMatched); + if (legacyDigestVerified && !legacyDigestMatched) + { + m_ReceivedNeedMoreDataForCompletePDU = false; + m_ReceivedTempPDU = null; + PublishReceiveError(S7Consts.errS7CommPlusDigestMismatch); + return; + } + dataPos += LegacyDigestFieldLength; + dataLen -= LegacyDigestFieldLength; + } // Copy data part to destination stream - m_ReceivedTempPDU.Write(PDU, pos, s7HeaderDataLen); + m_ReceivedTempPDU.Write(PDU, dataPos, dataLen); pos += s7HeaderDataLen; // If this is a fragmented PDU, then at this point no trailer if ((len - 4 - 4) == s7HeaderDataLen) @@ -330,14 +798,67 @@ private void OnDataReceived(byte[] PDU, int len) // If a complete (usable) PDU is received, add to the queue (threadsafe) for readout if (m_NewS7CommPlusReceived) { - // Push complete PDU to the queue - m_Mutex.WaitOne(); - m_ReceivedPDUs.Enqueue(m_ReceivedTempPDU); - m_Mutex.ReleaseMutex(); + m_ReceivedPDUs.Writer.TryWrite(new ReceivedS7PlusPdu(m_ReceivedTempPDU)); m_NewS7CommPlusReceived = false; } } + private void PublishReceiveError(int errorCode) + { + m_LastError = errorCode; + m_ReceivedPDUs.Writer.TryWrite(new ReceivedS7PlusPdu(null, errorCode)); + } + + private byte[] BuildLegacyAccumulatedDigestData(byte[] pdu, int bodyPos, int bodyLen, int previousBodyLen) + { + var digestData = new byte[previousBodyLen + bodyLen]; + var temp = m_ReceivedTempPDU.GetBuffer(); + Array.Copy(temp, 1, digestData, 0, previousBodyLen); + Array.Copy(pdu, bodyPos, digestData, previousBodyLen, bodyLen); + return digestData; + } + + internal void DebugOnDataReceivedForTests(byte[] pdu) + { + OnDataReceived(pdu, pdu.Length); + } + + internal int DebugReceiveNextS7plusPduForTests(int timeout, out MemoryStream pdu) + { + return ReceiveNextS7plusPdu(timeout, out pdu); + } + + internal void DebugResetReceiveDispatcherForTests() + { + m_ReceivedPDUs.Writer.TryComplete(); + m_ReceivedPDUs = CreateReceiveChannel(); + m_ReceivedTempPDU = null; + m_ReceivedPDU = null; + m_ReceivedNeedMoreDataForCompletePDU = false; + m_NewS7CommPlusReceived = false; + m_LastSentRequestForWait = null; + ClearNotificationQueues(); + m_LastError = 0; + } + + internal void DebugEnableLegacyDigestForTests(byte[] sessionKey) + { + m_LegacySessionKey = sessionKey; + m_LegacyDigestActive = true; + } + + internal bool DebugGetMultiVariablesRequestExceedsLegacySingleFrameForTests(IEnumerable addresses) + { + var request = new GetMultiVariablesRequest(ProtocolVersion.V3); + request.AddressList.AddRange(addresses); + return ExceedsLegacySingleFramePayload(request, GetMaxS7CommPlusPayloadSize(LegacyDigestFieldLength)); + } + + internal int DebugSendLegacyPayloadForTests(byte[] payload) + { + return SendS7plusPDUdata(payload, payload.Length, ProtocolVersion.V3); + } + private UInt16 GetWordAt(byte[] Buffer, int Pos) { return (UInt16)((Buffer[Pos] << 8) | Buffer[Pos + 1]); @@ -351,30 +872,26 @@ private void SetWordAt(byte[] Buffer, int Pos, UInt16 Value) private void printBuf(byte[] b) { - for (int i = 0; i < b.Length; i++) - { - Console.Write("0x" + String.Format("{0:X02} ", b[i])); - } - Console.Write(Environment.NewLine); + Trace.WriteLine(BitConverter.ToString(b)); } private int checkResponseWithIntegrity(IS7pRequest request, IS7pResponse response) { if (response == null) { - Console.WriteLine("checkResponseWithIntegrity: ERROR! response == null"); - return S7Consts.errIsoInvalidPDU; + //System.Diagnostics.Trace.WriteLine("checkResponseWithIntegrity: ERROR! response == null"); + return S7Consts.errIsoInvalidPDU4; } if (request.SequenceNumber != response.SequenceNumber) { - Console.WriteLine(String.Format("checkResponseWithIntegrity: ERROR! SeqenceNumber of Response ({0}) doesn't match Request ({1})", response.SequenceNumber, request.SequenceNumber)); - return S7Consts.errIsoInvalidPDU; + //System.Diagnostics.Trace.WriteLine(String.Format("checkResponseWithIntegrity: ERROR! SeqenceNumber of Response ({0}) doesn't match Request ({1})", response.SequenceNumber, request.SequenceNumber)); + return S7Consts.errIsoInvalidPDU5; } // Overflow is possible and allowed UInt32 reqIntegCheck = (UInt32)request.SequenceNumber + request.IntegrityId; if (response.IntegrityId != reqIntegCheck) { - Console.WriteLine(String.Format("checkResponseWithIntegrity: ERROR! IntegrityId of the Response ({0}) doesn't match Request ({1})", response.IntegrityId, reqIntegCheck)); + Trace.WriteLine(String.Format("checkResponseWithIntegrity: ERROR! IntegrityId of the Response ({0}) doesn't match Request ({1})", response.IntegrityId, reqIntegCheck)); // Don't return this as error so far } return 0; @@ -389,19 +906,97 @@ private int checkResponseWithIntegrity(IS7pRequest request, IS7pResponse respons /// PLC password (if set) /// read timeout in milliseconds (default: 5000 ms) /// - public int Connect(string address, string password = "", string username = "", int timeoutMs = 5000) + public bool IsConnected { - if (timeoutMs > 0) { - m_ReadTimeout = timeoutMs; + get { return m_client?.Connected == true; } + } + + public int Connect(string address, string password = "", string username = "", int timeoutMs = 5000, int port = 102, ushort localTsap = 0x0600, byte[] remoteTsap = null) + { + return ConnectTls(address, password, username, timeoutMs, port, localTsap, remoteTsap); + } + + internal int Connect(S7CommPlusClientOptions options) + { + if (options == null) + { + return S7Consts.errCliInvalidParams; + } + + switch (options.SecurityMode) + { + case S7CommPlusSecurityMode.Tls: + return ConnectAndSetNegotiatedMode(options, S7CommPlusSecurityMode.Tls); + case S7CommPlusSecurityMode.LegacyChallenge: + return ConnectAndSetNegotiatedMode(options, S7CommPlusSecurityMode.LegacyChallenge); + case S7CommPlusSecurityMode.Auto: + var tlsResult = ConnectAndSetNegotiatedMode(options, S7CommPlusSecurityMode.Tls); + if (tlsResult == 0) + { + return 0; + } + options.Logger.LogInformation("TLS connect to PLC {Address}:{Port} failed with {ErrorCode}; trying legacy challenge authentication.", options.Address, options.Port, tlsResult); + TryDisconnect(options.DisconnectTimeoutMilliseconds); + return ConnectAndSetNegotiatedMode(options, S7CommPlusSecurityMode.LegacyChallenge); + default: + return S7Consts.errCliInvalidParams; + } + } + + private int ConnectAndSetNegotiatedMode(S7CommPlusClientOptions options, S7CommPlusSecurityMode securityMode) + { + var result = securityMode == S7CommPlusSecurityMode.Tls + ? ConnectTls(options.Address, options.Password, options.Username, options.RequestTimeoutMilliseconds, options.Port, options.LocalTsap, options.RemoteTsapBytes, options.TlsBackend) + : ConnectLegacyChallenge(options); + if (result == 0) + { + m_NegotiatedSecurityMode = securityMode; + options.NegotiatedSecurityMode = securityMode; } + return result; + } + private void PrepareClient(string address, int timeoutMs, int port, ushort localTsap, byte[] remoteTsap) + { m_LastError = 0; - int res; - int Elapsed = Environment.TickCount; + m_LastErrorDetail = string.Empty; + m_LegacyDigestActive = false; + m_LegacySessionKey = null; + m_ServerSessionVersion = null; + m_ReceivedPDU = null; + m_ReceivedTempPDU = null; + m_ReceivedNeedMoreDataForCompletePDU = false; + m_NewS7CommPlusReceived = false; + m_LastSentRequestForWait = null; + ClearNotificationQueues(); + m_SessionId = 0; + m_SessionId2 = 0; + m_SequenceNumber = 0; + m_IntegrityId = 0; + m_IntegrityId_Set = 0; + dbInfoList = null; + typeInfoList.Clear(); + m_ReceivedPDUs.Writer.TryComplete(); + m_ReceivedPDUs = CreateReceiveChannel(); m_client = new S7Client(); m_client.OnDataReceived = this.OnDataReceived; + m_client.OnReceiveError = this.PublishReceiveError; + m_client.PLCPort = port; + m_client.ConnTimeout = timeoutMs; + m_client.RecvTimeout = timeoutMs; + m_client.SendTimeout = timeoutMs; + m_client.SetConnectionParams(address, localTsap, remoteTsap ?? Encoding.ASCII.GetBytes("SIMATIC-ROOT-HMI")); + } + + private int ConnectTls(string address, string password = "", string username = "", int timeoutMs = 5000, int port = 102, ushort localTsap = 0x0600, byte[] remoteTsap = null, S7CommPlusTlsBackend tlsBackend = S7CommPlusTlsBackend.OpenSsl) + { + if (timeoutMs > 0) { + m_ReadTimeout = timeoutMs; + } - m_client.SetConnectionParams(address, 0x0600, Encoding.ASCII.GetBytes("SIMATIC-ROOT-HMI")); + int res; + int Elapsed = Environment.TickCount; + PrepareClient(address, timeoutMs, port, localTsap, remoteTsap); res = m_client.Connect(); if (res != 0) return res; @@ -409,35 +1004,28 @@ public int Connect(string address, string password = "", string username = "", i #region Step 1: Unencrypted InitSSL Request / Response InitSslRequest sslReq = new InitSslRequest(ProtocolVersion.V1, 0 , 0); - res = SendS7plusFunctionObject(sslReq); + res = SendS7plusFunctionObjectAndWait(sslReq, m_ReadTimeout); if (res != 0) { m_client.Disconnect(); return res; } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - m_client.Disconnect(); - return m_LastError; - } InitSslResponse sslRes; sslRes = InitSslResponse.DeserializeFromPdu(m_ReceivedPDU); if (sslRes == null) { - Console.WriteLine("S7CommPlusConnection - Connect: InitSslResponse with Error!"); m_client.Disconnect(); - return m_LastError; + return S7Consts.errInitSslResponse; } #endregion #region Step 2: Activate TLS. Everything from here onwards is TLS encrypted. - res = m_client.SslActivate(); + res = m_client.SslActivate(tlsBackend); if (res != 0) { + m_LastErrorDetail = m_client.LastErrorDetail; m_client.Disconnect(); return res; } @@ -448,36 +1036,31 @@ public int Connect(string address, string password = "", string username = "", i var createObjReq = new CreateObjectRequest(ProtocolVersion.V1, 0, false); createObjReq.SetNullServerSessionData(); - res = SendS7plusFunctionObject(createObjReq); + res = SendS7plusFunctionObjectAndWait(createObjReq, m_ReadTimeout); if (res != 0) { + m_LastErrorDetail = m_client.LastErrorDetail; m_client.Disconnect(); return res; } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - m_client.Disconnect(); - return m_LastError; - } var createObjRes = CreateObjectResponse.DeserializeFromPdu(m_ReceivedPDU); if (createObjRes == null) { - Console.WriteLine("S7CommPlusConnection - Connect: CreateObjectResponse with Error!"); + //System.Diagnostics.Trace.WriteLine("S7CommPlusProtocolSession - Connect: CreateObjectResponse with Error!"); m_client.Disconnect(); - return S7Consts.errIsoInvalidPDU; + return S7Consts.errIsoInvalidPDU6; } // There are (always?) at least two IDs in the response. // Usually the first is used for polling data, and the 2nd for jobs which use notifications, e.g. alarming, subscriptions. m_SessionId = createObjRes.ObjectIds[0]; m_SessionId2 = createObjRes.ObjectIds[1]; - Console.WriteLine("S7CommPlusConnection - Connect: Using SessionId=0x" + String.Format("{0:X04}", m_SessionId)); + //System.Diagnostics.Trace.WriteLine("S7CommPlusProtocolSession - Connect: Using SessionId=0x" + String.Format("{0:X04}", m_SessionId)); // Evaluate Struct 314 PValue sval = createObjRes.ResponseObject.GetAttribute(Ids.ServerSessionVersion); ValueStruct serverSession = (ValueStruct)sval; + m_ServerSessionVersion = serverSession; #endregion @@ -485,32 +1068,25 @@ public int Connect(string address, string password = "", string username = "", i var setMultiVarReq = new SetMultiVariablesRequest(ProtocolVersion.V2); setMultiVarReq.SetSessionSetupData(m_SessionId, serverSession); - res = SendS7plusFunctionObject(setMultiVarReq); + res = SendS7plusFunctionObjectAndWait(setMultiVarReq, m_ReadTimeout); if (res != 0) { m_client.Disconnect(); return res; } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - m_client.Disconnect(); - return m_LastError; - } var setMultiVarRes = SetMultiVariablesResponse.DeserializeFromPdu(m_ReceivedPDU); if (setMultiVarRes == null) { - Console.WriteLine("S7CommPlusConnection - Connect: SetMultiVariablesResponse with Error!"); + //System.Diagnostics.Trace.WriteLine("S7CommPlusProtocolSession - Connect: SetMultiVariablesResponse with Error!"); m_client.Disconnect(); - return S7Consts.errIsoInvalidPDU; + return S7Consts.errIsoInvalidPDU7; } #endregion #region Step 5: Read SystemLimits - res = m_CommRessources.ReadMax(this); + res = m_CommunicationResources.ReadMax(this); if (res != 0) { m_client.Disconnect(); @@ -527,14 +1103,141 @@ public int Connect(string address, string password = "", string username = "", i #endregion // If everything has been error-free up to this point, then the connection has been established successfully. - Console.WriteLine("S7CommPlusConnection - Connect: Time for connection establishment: " + (Environment.TickCount - Elapsed) + " ms."); + Trace.WriteLine("S7CommPlusProtocolSession - Connect: Time for connection establishment: " + (Environment.TickCount - Elapsed) + " ms."); return 0; } + public int Legitimate(string password, string username = "") + { + if (!IsConnected) + { + return S7Consts.errTCPNotConnected; + } + + if (m_ServerSessionVersion == null) + { + return S7Consts.errCliFirmwareNotSupported; + } + + return legitimate(m_ServerSessionVersion, password ?? string.Empty, username ?? string.Empty); + } + + public int GetCommunicationResources(out S7CommPlusCommunicationResourceSnapshot resources) + { + resources = m_CommunicationResources ?? new S7CommPlusCommunicationResourceSnapshot(); + if (!IsConnected) + { + return S7Consts.errTCPNotConnected; + } + + var result = resources.ReadMax(this); + if (result != 0) + { + return result; + } + + return resources.ReadFree(this); + } + public void Disconnect() { - DeleteObject(m_SessionId); - m_client.Disconnect(); + TryDisconnect(m_ReadTimeout); + } + + public int TryDisconnect(int timeoutMs = 2000) + { + int res = 0; + var oldTimeout = m_ReadTimeout; + if (timeoutMs > 0) + { + m_ReadTimeout = timeoutMs; + } + + try + { + if (m_client?.Connected == true && m_SessionId != 0) + { + res = DeleteObject(m_SessionId); + } + } + catch + { + res = S7Consts.errTCPDataSend; + } + finally + { + try + { + var disconnectResult = m_client?.Disconnect(timeoutMs) ?? 0; + if (res == 0) + { + res = disconnectResult; + } + } + catch + { + if (res == 0) + { + res = S7Consts.errTCPDataReceive; + } + } + + m_ReadTimeout = oldTimeout; + m_ReceivedPDU = null; + m_ReceivedTempPDU = null; + m_ReceivedPDUs.Writer.TryComplete(); + m_ReceivedPDUs = CreateReceiveChannel(); + m_ReceivedNeedMoreDataForCompletePDU = false; + m_NewS7CommPlusReceived = false; + m_LastSentRequestForWait = null; + ClearNotificationQueues(); + m_SessionId = 0; + m_SessionId2 = 0; + m_SequenceNumber = 0; + m_IntegrityId = 0; + m_IntegrityId_Set = 0; + dbInfoList = null; + typeInfoList.Clear(); + } + + return res; + } + + internal int CloseTransport(int timeoutMs = 2000) + { + var oldTimeout = m_ReadTimeout; + if (timeoutMs > 0) + { + m_ReadTimeout = timeoutMs; + } + + try + { + return m_client?.Disconnect(timeoutMs) ?? 0; + } + catch + { + return S7Consts.errTCPDataReceive; + } + finally + { + m_ReadTimeout = oldTimeout; + m_ReceivedPDU = null; + m_ReceivedTempPDU = null; + m_ReceivedPDUs.Writer.TryComplete(); + m_ReceivedPDUs = CreateReceiveChannel(); + m_ReceivedNeedMoreDataForCompletePDU = false; + m_NewS7CommPlusReceived = false; + m_LastSentRequestForWait = null; + ClearNotificationQueues(); + m_SessionId = 0; + m_SessionId2 = 0; + m_SequenceNumber = 0; + m_IntegrityId = 0; + m_IntegrityId_Set = 0; + dbInfoList = null; + typeInfoList.Clear(); + } } /// @@ -547,12 +1250,10 @@ private int DeleteObject(uint deleteObjectId) int res; var delObjReq = new DeleteObjectRequest(ProtocolVersion.V2); delObjReq.DeleteObjectId = deleteObjectId; - res = SendS7plusFunctionObject(delObjReq); - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) + res = SendS7plusFunctionObjectAndWait(delObjReq, m_ReadTimeout); + if (res != 0) { - return m_LastError; + return res; } // If we delete our own session id, then there's no IntegrityId in the response. // And the error code gives an error, but not a fatal one. @@ -561,7 +1262,7 @@ private int DeleteObject(uint deleteObjectId) if (deleteObjectId == m_SessionId) { var delObjRes = DeleteObjectResponse.DeserializeFromPdu(m_ReceivedPDU, false); - Trace.WriteLine("S7CommPlusConnection - DeleteSession: Deleted our own Session Id object, not checking the response."); + Trace.WriteLine("S7CommPlusProtocolSession - DeleteSession: Deleted our own Session Id object, not checking the response."); m_SessionId = 0; // not valid anymore m_SessionId2 = 0; } @@ -575,7 +1276,7 @@ private int DeleteObject(uint deleteObjectId) } if (delObjRes.ReturnValue != 0) { - Console.WriteLine("S7CommPlusConnection - DeleteSession: Executed with Error! ReturnValue=" + delObjRes.ReturnValue); + Trace.WriteLine("S7CommPlusProtocolSession - DeleteSession: Executed with Error! ReturnValue=" + delObjRes.ReturnValue); res = -1; } } @@ -594,10 +1295,16 @@ public int ReadValues(List addresslist, out List values, ou values.Add(null); errors.Add(0xffffffffffffffff); } + if (addresslist.Count == 0) + { + return 0; + } // Split request into chunks, taking the MaxTags per request into account int chunk_startIndex = 0; int count_perChunk = 0; + int maxTagsPerRequest = Math.Max(1, m_CommunicationResources.TagsPerReadRequestMax); + int maxLegacyPayloadSize = GetLegacySingleFramePayloadLimit(ProtocolVersion.V3); do { int res; @@ -605,18 +1312,21 @@ public int ReadValues(List addresslist, out List values, ou getMultiVarReq.AddressList.Clear(); count_perChunk = 0; - while (count_perChunk < m_CommRessources.TagsPerReadRequestMax && (chunk_startIndex + count_perChunk) < addresslist.Count) + while (count_perChunk < maxTagsPerRequest && (chunk_startIndex + count_perChunk) < addresslist.Count) { getMultiVarReq.AddressList.Add(addresslist[chunk_startIndex + count_perChunk]); + if (count_perChunk > 0 && ExceedsLegacySingleFramePayload(getMultiVarReq, maxLegacyPayloadSize)) + { + getMultiVarReq.AddressList.RemoveAt(getMultiVarReq.AddressList.Count - 1); + break; + } count_perChunk++; } - res = SendS7plusFunctionObject(getMultiVarReq); - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) + res = SendS7plusFunctionObjectAndWait(getMultiVarReq, m_ReadTimeout); + if (res != 0) { - return m_LastError; + return res; } var getMultiVarRes = GetMultiVariablesResponse.DeserializeFromPdu(m_ReceivedPDU); @@ -628,11 +1338,11 @@ public int ReadValues(List addresslist, out List values, ou // ReturnValue shows also an error, if only one single variable could not be read if (getMultiVarRes.ReturnValue != 0) { - Console.WriteLine("S7CommPlusConnection - ReadValues: Executed with Error! ReturnValue=" + getMultiVarRes.ReturnValue); + Trace.WriteLine("S7CommPlusProtocolSession - ReadValues: Executed with Error! ReturnValue=" + getMultiVarRes.ReturnValue); } - // TODO: If a variable could not be read, there is no value, but there is an ErrorValue. - // The user must therefore check whether Value != null. Maybe there's a more elegant solution. + // If a variable could not be read, there is no value, but there is an ErrorValue. + // The production client maps this to per-item batch status. foreach (var v in getMultiVarRes.Values) { values[chunk_startIndex + (int)v.Key - 1] = v.Value; @@ -655,39 +1365,42 @@ public int WriteValues(List addresslist, List values, out L { int res; errors = new List(); + if (addresslist.Count != values.Count) + { + return S7Consts.errCliInvalidParams; + } for (int i = 0; i < addresslist.Count; i++) { // Initialize to no error value, as there's no explicit value for write success. errors.Add(0); } + if (addresslist.Count == 0) + { + return 0; + } // Split request into chunks, taking the MaxTags per request into account int chunk_startIndex = 0; int count_perChunk = 0; + int maxTagsPerRequest = Math.Max(1, m_CommunicationResources.TagsPerWriteRequestMax); do { var setMultiVarReq = new SetMultiVariablesRequest(ProtocolVersion.V2); setMultiVarReq.AddressListVar.Clear(); setMultiVarReq.ValueList.Clear(); count_perChunk = 0; - while (count_perChunk < m_CommRessources.TagsPerWriteRequestMax && (chunk_startIndex + count_perChunk) < addresslist.Count) + while (count_perChunk < maxTagsPerRequest && (chunk_startIndex + count_perChunk) < addresslist.Count) { setMultiVarReq.AddressListVar.Add(addresslist[chunk_startIndex + count_perChunk]); setMultiVarReq.ValueList.Add(values[chunk_startIndex + count_perChunk]); count_perChunk++; } - res = SendS7plusFunctionObject(setMultiVarReq); + res = SendS7plusFunctionObjectAndWait(setMultiVarReq, m_ReadTimeout); if (res != 0) { return res; } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } var setMultiVarRes = SetMultiVariablesResponse.DeserializeFromPdu(m_ReceivedPDU); res = checkResponseWithIntegrity(setMultiVarReq, setMultiVarRes); @@ -698,7 +1411,7 @@ public int WriteValues(List addresslist, List values, out L // ReturnValue shows also an error, if only one single variable could not be written if (setMultiVarRes.ReturnValue != 0) { - Console.WriteLine("S7CommPlusConnection - WriteValues: Write with errors. ReturnValue=" + setMultiVarRes.ReturnValue); + Trace.WriteLine("S7CommPlusProtocolSession - WriteValues: Write with errors. ReturnValue=" + setMultiVarRes.ReturnValue); } foreach (var ev in setMultiVarRes.ErrorValues) @@ -720,26 +1433,30 @@ public int SetPlcOperatingState(Int32 state) setVarReq.Address = Ids.CPUexecUnit_operatingStateReq; setVarReq.Value = new ValueDInt(state); - res = SendS7plusFunctionObject(setVarReq); + res = SendS7plusFunctionObjectAndWait(setVarReq, m_ReadTimeout); if (res != 0) { m_client.Disconnect(); return res; } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - m_client.Disconnect(); - return m_LastError; - } var setVarRes = SetVariableResponse.DeserializeFromPdu(m_ReceivedPDU); if (setVarRes == null) { - Console.WriteLine("S7CommPlusConnection - Connect: SetVariableResponse with Error!"); + //System.Diagnostics.Trace.WriteLine("S7CommPlusProtocolSession - Connect: SetVariableResponse with Error!"); m_client.Disconnect(); - return S7Consts.errIsoInvalidPDU; + return S7Consts.errIsoInvalidPDU12; + } + + res = checkResponseWithIntegrity(setVarReq, setVarRes); + if (res != 0) + { + return res; + } + + if (setVarRes.ReturnValue != 0) + { + return S7Consts.errCliInvalidParams; } return 0; @@ -768,17 +1485,11 @@ public int Browse(out List varInfoList) exploreReq.AddressList.Add(Ids.Block_BlockNumber); exploreReq.AddressList.Add(Ids.ASObjectES_Comment); - res = SendS7plusFunctionObject(exploreReq); + res = SendS7plusFunctionObjectAndWait(exploreReq, m_ReadTimeout); if (res != 0) { return res; } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); res = checkResponseWithIntegrity(exploreReq, exploreRes); @@ -858,7 +1569,7 @@ public int Browse(out List varInfoList) else { // On error, set the relid to zero, will be removed from the list in the next step. - // TODO: Report this as an error? + Trace.WriteLine(String.Format("Explore: Skipping block type info relation for {0}, item error 0x{1:X}.", exploreData[i].db_name, errors[i])); var data = exploreData[i]; data.db_block_ti_relid = 0; exploreData[i] = data; @@ -890,17 +1601,11 @@ public int Browse(out List varInfoList) exploreReq.ExploreChildsRecursive = 1; exploreReq.ExploreParents = 0; - res = SendS7plusFunctionObject(exploreReq); + res = SendS7plusFunctionObjectAndWait(exploreReq, m_ReadTimeout); if (res != 0) { return res; } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } #endregion #region Process the response, and build the complete variables list @@ -1080,6 +1785,7 @@ private PlcTag browsePlcTagBySymbol(uint ti_relid, ref string symbol, VarInfo va if (idx < 0) return null; PVartypeListElement varType = pObj.VartypeList.Elements[idx]; varInfo.AccessSequence += "." + String.Format("{0:X}", varType.LID); + AddSymbolCrcSegment(varInfo, levelName, varType); bool is1Dim = false; if (varType.OffsetInfoType.Is1Dim()) { @@ -1100,7 +1806,7 @@ private PlcTag browsePlcTagBySymbol(uint ti_relid, ref string symbol, VarInfo va { if (symbol.Length <= 0 && varType.Softdatatype == Softdatatype.S7COMMP_SOFTDATATYPE_DTL) { - return PlcTags.TagFactory(varInfo.Name, new ItemAddress(varInfo.AccessSequence), varType.Softdatatype, is1Dim); + return PlcTags.TagFactory(varInfo.Name, CreateItemAddress(varInfo), varType.Softdatatype, is1Dim); } if (symbol.Length <= 0) { @@ -1114,12 +1820,40 @@ private PlcTag browsePlcTagBySymbol(uint ti_relid, ref string symbol, VarInfo va } else { - return PlcTags.TagFactory(varInfo.Name, new ItemAddress(varInfo.AccessSequence), varType.Softdatatype, is1Dim); + return PlcTags.TagFactory(varInfo.Name, CreateItemAddress(varInfo), varType.Softdatatype, is1Dim); + } + } + + private static void AddSymbolCrcSegment(VarInfo varInfo, string levelName, PVartypeListElement varType) + { + if (varInfo.SymbolCrcPath == null) + { + varInfo.SymbolCrcPath = new List(); + } + + if (varType.OffsetInfoType.Is1Dim() || varType.OffsetInfoType.IsMDim()) + { + varInfo.SymbolCrcPath.Add(S7CommPlusSymbolCrc.PathSegment.Array( + levelName, + varType.Softdatatype, + Browser.GetArrayLowerBound(varType.OffsetInfoType))); + } + else + { + varInfo.SymbolCrcPath.Add(S7CommPlusSymbolCrc.PathSegment.Member(levelName, varType.Softdatatype)); } } + private static ItemAddress CreateItemAddress(VarInfo varInfo) + { + var address = new ItemAddress(varInfo.AccessSequence); + address.SymbolCrc = S7CommPlusSymbolCrc.ComputeFromSegments(varInfo.SymbolCrcPath); + varInfo.SymbolCrc = address.SymbolCrc; + return address; + } + /// - /// Get the plc tag for the given plc tag symbol. + /// Get the plc tag for the given plc tag symbol. /// /// plc tag symbol /// plc tag, returns null if plc tag could not be found @@ -1127,6 +1861,7 @@ public PlcTag getPlcTagBySymbol(string symbol) { VarInfo varInfo = new VarInfo(); varInfo.Name = symbol; + varInfo.SymbolCrcPath = new List(); // make sure we have the db list if (dbInfoList == null) { @@ -1145,19 +1880,33 @@ public PlcTag getPlcTagBySymbol(string symbol) symbol = varInfo.Name; // Merker varInfo.AccessSequence = String.Format("{0:X}", Ids.NativeObjects_theMArea_Rid); + varInfo.SymbolCrcPath.Clear(); PlcTag tag = browsePlcTagBySymbol(0x90030000, ref symbol, varInfo); if (tag != null) return tag; symbol = varInfo.Name; // Outputs varInfo.AccessSequence = String.Format("{0:X}", Ids.NativeObjects_theQArea_Rid); + varInfo.SymbolCrcPath.Clear(); tag = browsePlcTagBySymbol(0x90020000, ref symbol, varInfo); if (tag != null) return tag; symbol = varInfo.Name; // Inputs varInfo.AccessSequence = String.Format("{0:X}", Ids.NativeObjects_theIArea_Rid); + varInfo.SymbolCrcPath.Clear(); tag = browsePlcTagBySymbol(0x90010000, ref symbol, varInfo); if (tag != null) return tag; - // TODO: implement s5timers and counters... no one uses them anymore anyway + symbol = varInfo.Name; + // S7 timers + varInfo.AccessSequence = String.Format("{0:X}", Ids.NativeObjects_theS7Timers_Rid); + varInfo.SymbolCrcPath.Clear(); + tag = browsePlcTagBySymbol(0x90050000, ref symbol, varInfo); + if (tag != null) return tag; + symbol = varInfo.Name; + // S7 counters + varInfo.AccessSequence = String.Format("{0:X}", Ids.NativeObjects_theS7Counters_Rid); + varInfo.SymbolCrcPath.Clear(); + tag = browsePlcTagBySymbol(0x90060000, ref symbol, varInfo); + if (tag != null) return tag; } return null; } @@ -1214,17 +1963,11 @@ public int GetListOfDatablocks(out List dbInfoList) exploreReq.FilterData = filter; - res = SendS7plusFunctionObject(exploreReq); + res = SendS7plusFunctionObjectAndWait(exploreReq, m_ReadTimeout); if (res != 0) { return res; } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } var exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); res = checkResponseWithIntegrity(exploreReq, exploreRes); @@ -1260,7 +2003,7 @@ public int GetListOfDatablocks(out List dbInfoList) // Get the TypeInfo RID to RelId from the first response - // With LID=1 we get the RID back. With this number we can explore further + // With LID=1 we get the RID back. With this number we can explore further // informations of this datablock. // This is neccessary, because informations about instance DBs (e.g. TON) you // don't get by the RID of the DB, instead of exploring the TON Type RID. @@ -1324,17 +2067,11 @@ public int GetTypeInformation(uint exploreId, out List objList) exploreReq.ExploreChildsRecursive = 1; exploreReq.ExploreParents = 0; - res = SendS7plusFunctionObject(exploreReq); + res = SendS7plusFunctionObjectAndWait(exploreReq, m_ReadTimeout); if (res != 0) { return res; } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } var exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); res = checkResponseWithIntegrity(exploreReq, exploreRes); @@ -1354,7 +2091,7 @@ public int GetTypeInformation(uint exploreId, out List objList) /// I/Q/M/C/T: .... /// Datablock: .... /// As "ID" the number for the variable identification is used. - /// + /// /// xml_dbcomment: /// The xml-value description generated from our own value xml-serialization for WStringSparseArray. The value key is the language id. /// Example: @@ -1383,17 +2120,11 @@ public int GetCommentsXml(uint relid, out string xml_linecomment, out string xml exploreReq.AddressList.Add(Ids.ASObjectES_Comment); exploreReq.AddressList.Add(Ids.DataInterface_LineComments); - res = SendS7plusFunctionObject(exploreReq); + res = SendS7plusFunctionObjectAndWait(exploreReq, m_ReadTimeout); if (res != 0) { return res; } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } var exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); res = checkResponseWithIntegrity(exploreReq, exploreRes); diff --git a/src/S7CommPlusDriver/S7CommPlusResults.cs b/src/S7CommPlusDriver/S7CommPlusResults.cs new file mode 100644 index 0000000..d1edaaf --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusResults.cs @@ -0,0 +1,56 @@ +using S7CommPlusDriver.ClientApi; +using System.Collections.Generic; + +namespace S7CommPlusDriver +{ + public sealed class S7CommPlusReadResult + { + public S7CommPlusReadResult(ItemAddress address, object value, ulong itemError) + { + Address = address; + Value = value; + ItemError = itemError; + } + + public ItemAddress Address { get; } + public object Value { get; } + public ulong ItemError { get; } + public bool IsSuccess => ItemError == 0; + } + + public sealed class S7CommPlusTagReadResult + { + public S7CommPlusTagReadResult(PlcTag tag, ulong itemError) + { + Tag = tag; + ItemError = itemError; + } + + public PlcTag Tag { get; } + public ulong ItemError { get; } + public bool IsSuccess => ItemError == 0; + } + + public sealed class S7CommPlusWriteResult + { + public S7CommPlusWriteResult(ItemAddress address, ulong itemError) + { + Address = address; + ItemError = itemError; + } + + public ItemAddress Address { get; } + public ulong ItemError { get; } + public bool IsSuccess => ItemError == 0; + } + + public sealed class S7CommPlusBatchResult + { + public S7CommPlusBatchResult(IReadOnlyList items) + { + Items = items; + } + + public IReadOnlyList Items { get; } + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusSecurityMode.cs b/src/S7CommPlusDriver/S7CommPlusSecurityMode.cs new file mode 100644 index 0000000..4f8efa3 --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusSecurityMode.cs @@ -0,0 +1,9 @@ +namespace S7CommPlusDriver +{ + public enum S7CommPlusSecurityMode + { + Tls = 0, + LegacyChallenge = 1, + Auto = 2 + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusSubscriptions.cs b/src/S7CommPlusDriver/S7CommPlusSubscriptions.cs new file mode 100644 index 0000000..5854fcd --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusSubscriptions.cs @@ -0,0 +1,428 @@ +using S7CommPlusDriver.Alarming; +using S7CommPlusDriver.ClientApi; +using S7CommPlusDriver.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace S7CommPlusDriver +{ + public enum S7CommPlusSubscriptionState + { + Created, + Running, + Stopping, + Stopped, + Faulted + } + + public sealed class S7CommPlusSubscriptionOptions + { + public ushort CycleTimeMilliseconds { get; set; } = 1000; + public TimeSpan NotificationTimeout { get; set; } = TimeSpan.FromSeconds(5); + public short InitialCreditLimit { get; set; } = S7CommPlusProtocolConstants.DefaultSubscriptionCreditLimit; + public short CreditLimitStep { get; set; } = S7CommPlusProtocolConstants.DefaultSubscriptionCreditLimitStep; + public int MaxConsecutiveTimeoutsBeforeFault { get; set; } + public bool DeleteOnStop { get; set; } = true; + + internal int NotificationTimeoutMilliseconds => checked((int)NotificationTimeout.TotalMilliseconds); + + internal void Validate(bool requireCycleTime) + { + if (requireCycleTime && CycleTimeMilliseconds == 0) + { + throw new ArgumentOutOfRangeException(nameof(CycleTimeMilliseconds), "Cycle time must be greater than zero."); + } + if (NotificationTimeout <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(NotificationTimeout), "Notification timeout must be greater than zero."); + } + if (InitialCreditLimit == 0 || InitialCreditLimit < -1 || InitialCreditLimit > 255) + { + throw new ArgumentOutOfRangeException(nameof(InitialCreditLimit), "Initial credit limit must be -1 or between 1 and 255."); + } + if (CreditLimitStep < 0 || CreditLimitStep > 255) + { + throw new ArgumentOutOfRangeException(nameof(CreditLimitStep), "Credit limit step must be between 0 and 255."); + } + if (MaxConsecutiveTimeoutsBeforeFault < 0) + { + throw new ArgumentOutOfRangeException(nameof(MaxConsecutiveTimeoutsBeforeFault), "Timeout fault threshold must be zero or greater."); + } + } + + internal S7CommPlusSubscriptionOptions Clone() + { + return (S7CommPlusSubscriptionOptions)MemberwiseClone(); + } + } + + public sealed class S7CommPlusSubscriptionStateChangedEventArgs : EventArgs + { + public S7CommPlusSubscriptionStateChangedEventArgs(S7CommPlusSubscriptionState oldState, S7CommPlusSubscriptionState newState, Exception exception) + { + OldState = oldState; + NewState = newState; + Exception = exception; + } + + public S7CommPlusSubscriptionState OldState { get; } + public S7CommPlusSubscriptionState NewState { get; } + public Exception Exception { get; } + } + + public sealed class S7CommPlusSubscriptionErrorEventArgs : EventArgs + { + public S7CommPlusSubscriptionErrorEventArgs(S7CommPlusException exception) + { + Exception = exception ?? throw new ArgumentNullException(nameof(exception)); + } + + public S7CommPlusException Exception { get; } + } + + public abstract class S7CommPlusSubscription : IAsyncDisposable + { + private readonly CancellationTokenSource _stopCts = new CancellationTokenSource(); + private readonly object _stateLock = new object(); + private Task _completion = Task.CompletedTask; + private int _stopRequested; + private S7CommPlusSubscriptionState _state = S7CommPlusSubscriptionState.Created; + private EventHandler _communicationError; + + public event EventHandler StateChanged; + public event EventHandler CommunicationError + { + add + { + _communicationError += value; + var fault = FaultException; + if (fault != null && value != null) + { + _ = Task.Run(() => value(this, new S7CommPlusSubscriptionErrorEventArgs(fault))); + } + } + remove + { + _communicationError -= value; + } + } + + public S7CommPlusSubscriptionState State + { + get + { + lock (_stateLock) + { + return _state; + } + } + } + + public Task Completion => _completion; + public S7CommPlusException FaultException { get; private set; } + + public async Task StopAsync(CancellationToken cancellationToken = default) + { + if (Interlocked.Exchange(ref _stopRequested, 1) == 0) + { + if (State == S7CommPlusSubscriptionState.Created || State == S7CommPlusSubscriptionState.Running) + { + SetState(S7CommPlusSubscriptionState.Stopping); + } + _stopCts.Cancel(); + } + + try + { + await _completion.WaitAsync(cancellationToken).ConfigureAwait(false); + } + catch when (State == S7CommPlusSubscriptionState.Faulted || FaultException != null) + { + } + } + + public async ValueTask DisposeAsync() + { + await StopAsync().ConfigureAwait(false); + _stopCts.Dispose(); + } + + internal bool IsStopRequested => _stopRequested != 0 || _stopCts.IsCancellationRequested; + internal CancellationToken StopToken => _stopCts.Token; + + internal void Start(Func runAsync) + { + if (runAsync == null) + { + throw new ArgumentNullException(nameof(runAsync)); + } + + SetState(S7CommPlusSubscriptionState.Running); + _completion = Task.Run(async () => + { + try + { + await runAsync(_stopCts.Token).ConfigureAwait(false); + if (State != S7CommPlusSubscriptionState.Faulted) + { + SetState(S7CommPlusSubscriptionState.Stopped); + } + } + catch (OperationCanceledException) when (IsStopRequested) + { + SetState(S7CommPlusSubscriptionState.Stopped); + } + catch (S7CommPlusException ex) + { + MarkFaulted(ex); + throw; + } + }); + } + + internal void MarkFaulted(S7CommPlusException exception) + { + if (State == S7CommPlusSubscriptionState.Faulted && FaultException != null) + { + return; + } + + FaultException = exception; + _communicationError?.Invoke(this, new S7CommPlusSubscriptionErrorEventArgs(exception)); + SetState(S7CommPlusSubscriptionState.Faulted, exception); + } + + private void SetState(S7CommPlusSubscriptionState newState, Exception exception = null) + { + S7CommPlusSubscriptionState oldState; + lock (_stateLock) + { + oldState = _state; + if (oldState == newState) + { + return; + } + _state = newState; + } + + StateChanged?.Invoke(this, new S7CommPlusSubscriptionStateChangedEventArgs(oldState, newState, exception)); + } + } + + public sealed class S7CommPlusTagNotificationEventArgs : EventArgs + { + public S7CommPlusTagNotificationEventArgs(S7CommPlusTagNotification notification) + { + Notification = notification ?? throw new ArgumentNullException(nameof(notification)); + } + + public S7CommPlusTagNotification Notification { get; } + } + + public sealed class S7CommPlusTagNotification + { + public S7CommPlusTagNotification(DateTime timestamp, uint sequenceNumber, byte creditTick, IReadOnlyList items) + { + Timestamp = timestamp; + SequenceNumber = sequenceNumber; + CreditTick = creditTick; + Items = items ?? throw new ArgumentNullException(nameof(items)); + } + + public DateTime Timestamp { get; } + public uint SequenceNumber { get; } + public byte CreditTick { get; } + public IReadOnlyList Items { get; } + } + + public sealed class S7CommPlusTagNotificationItem + { + internal S7CommPlusTagNotificationItem(uint itemReferenceId, PlcTag tag, object value, ulong itemError) + { + ItemReferenceId = itemReferenceId; + Tag = tag; + Value = value; + ItemError = itemError; + } + + public uint ItemReferenceId { get; } + public PlcTag Tag { get; } + public object Value { get; } + public ulong ItemError { get; } + public bool IsSuccess => ItemError == 0; + } + + public sealed class S7CommPlusTagSubscription : S7CommPlusSubscription + { + internal S7CommPlusTagSubscription(IReadOnlyDictionary tagsByReferenceId) + { + TagsByReferenceId = tagsByReferenceId ?? throw new ArgumentNullException(nameof(tagsByReferenceId)); + } + + public event EventHandler NotificationReceived; + public IReadOnlyDictionary TagsByReferenceId { get; } + + internal void Publish(Notification notification) + { + var items = new List(); + foreach (var value in notification.Values) + { + TagsByReferenceId.TryGetValue(value.Key, out var tag); + items.Add(new S7CommPlusTagNotificationItem(value.Key, tag, value.Value, 0)); + } + foreach (var returnValue in notification.ReturnValues) + { + TagsByReferenceId.TryGetValue(returnValue.Key, out var tag); + items.Add(new S7CommPlusTagNotificationItem(returnValue.Key, tag, null, returnValue.Value)); + } + + var tagNotification = new S7CommPlusTagNotification( + notification.Add1Timestamp, + notification.NotificationSequenceNumber, + notification.NotificationCreditTick, + items.OrderBy(item => item.ItemReferenceId).ToList()); + NotificationReceived?.Invoke(this, new S7CommPlusTagNotificationEventArgs(tagNotification)); + } + } + + public sealed class S7CommPlusAlarmNotificationEventArgs : EventArgs + { + public S7CommPlusAlarmNotificationEventArgs(S7CommPlusAlarmNotification notification) + { + Notification = notification ?? throw new ArgumentNullException(nameof(notification)); + } + + public S7CommPlusAlarmNotification Notification { get; } + } + + public sealed class S7CommPlusAlarmNotification + { + public S7CommPlusAlarmNotification(DateTime timestamp, uint sequenceNumber, byte creditTick, byte returnValue, IReadOnlyList alarms) + { + Timestamp = timestamp; + SequenceNumber = sequenceNumber; + CreditTick = creditTick; + ReturnValue = returnValue; + Alarms = alarms ?? throw new ArgumentNullException(nameof(alarms)); + } + + public DateTime Timestamp { get; } + public uint SequenceNumber { get; } + public byte CreditTick { get; } + public byte ReturnValue { get; } + public IReadOnlyList Alarms { get; } + public bool IsSuccess => (S7CommPlusNotificationReturnCode)ReturnValue == S7CommPlusNotificationReturnCode.AlarmObject; + } + + public sealed class S7CommPlusAlarmSubscriptionWithSnapshot : IAsyncDisposable + { + internal S7CommPlusAlarmSubscriptionWithSnapshot(IReadOnlyList activeAlarms, S7CommPlusAlarmSubscription subscription) + { + ActiveAlarms = activeAlarms ?? Array.Empty(); + Subscription = subscription ?? throw new ArgumentNullException(nameof(subscription)); + } + + public IReadOnlyList ActiveAlarms { get; } + public S7CommPlusAlarmSubscription Subscription { get; } + + public ValueTask DisposeAsync() + { + return Subscription.DisposeAsync(); + } + } + + public sealed class S7CommPlusAlarmSubscription : S7CommPlusSubscription + { + private const int MaxReplayNotifications = 256; + private readonly object _notificationLock = new object(); + private readonly Queue _notificationReplay = new Queue(); + private readonly Func _textListResolver; + private EventHandler _notificationReceived; + + internal S7CommPlusAlarmSubscription(int alarmTextLanguageId) + : this(alarmTextLanguageId, null) + { + } + + internal S7CommPlusAlarmSubscription(int alarmTextLanguageId, Func textListResolver) + { + AlarmTextLanguageId = alarmTextLanguageId; + _textListResolver = textListResolver; + } + + public event EventHandler NotificationReceived + { + add + { + S7CommPlusAlarmNotification[] replay; + lock (_notificationLock) + { + _notificationReceived += value; + replay = _notificationReplay.ToArray(); + } + + if (value != null && replay.Length > 0) + { + _ = Task.Run(() => + { + foreach (var notification in replay) + { + value(this, new S7CommPlusAlarmNotificationEventArgs(notification)); + } + }); + } + } + remove + { + lock (_notificationLock) + { + _notificationReceived -= value; + } + } + } + + public int AlarmTextLanguageId { get; } + /// + /// True when the subscription requested every alarm text language instead of one LCID. + /// + public bool ReceivesAllAlarmTextLanguages => AlarmTextLanguageId == 0; + + internal void Publish(Notification notification) + { + var alarms = new List(); + if (notification.P2Objects != null) + { + foreach (var alarmObject in notification.P2Objects) + { + var alarm = S7CommPlusAlarm.FromNotificationObject(alarmObject, AlarmTextLanguageId, _textListResolver); + if (alarm != null) + { + alarms.Add(alarm); + } + } + } + + var alarmNotification = new S7CommPlusAlarmNotification( + notification.Add1Timestamp, + notification.NotificationSequenceNumber, + notification.NotificationCreditTick, + notification.P2ReturnValue, + alarms); + EventHandler handler; + lock (_notificationLock) + { + _notificationReplay.Enqueue(alarmNotification); + while (_notificationReplay.Count > MaxReplayNotifications) + { + _notificationReplay.Dequeue(); + } + handler = _notificationReceived; + } + + handler?.Invoke(this, new S7CommPlusAlarmNotificationEventArgs(alarmNotification)); + } + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusTextLists.cs b/src/S7CommPlusDriver/S7CommPlusTextLists.cs new file mode 100644 index 0000000..7820ec2 --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusTextLists.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace S7CommPlusDriver +{ + public enum S7CommPlusTextListScope + { + LanguageIndependent, + LanguageSpecific + } + + public enum S7CommPlusTextListType + { + Unknown, + User, + System + } + + public sealed class S7CommPlusTextListEntry + { + public S7CommPlusTextListEntry(int from, int to, string text) + { + From = from; + To = to; + Text = text ?? String.Empty; + } + + public int From { get; } + public int To { get; } + public string Text { get; } + public bool IsRange => From != To; + } + + public sealed class S7CommPlusTextList + { + private readonly Dictionary _entriesByValue; + + public S7CommPlusTextList(int listId, int languageId, S7CommPlusTextListScope scope, IEnumerable entries) + : this(listId, languageId, scope, S7CommPlusTextListType.Unknown, entries) + { + } + + public S7CommPlusTextList(int listId, int languageId, S7CommPlusTextListScope scope, S7CommPlusTextListType textListType, IEnumerable entries) + { + ListId = listId; + LanguageId = languageId; + Scope = scope; + TextListType = textListType; + Entries = (entries ?? Enumerable.Empty()).ToList().AsReadOnly(); + _entriesByValue = Entries + .Where(entry => !entry.IsRange) + .GroupBy(entry => entry.From) + .ToDictionary(group => group.Key, group => group.First()); + } + + public int ListId { get; } + public int LanguageId { get; } + public S7CommPlusTextListScope Scope { get; } + public S7CommPlusTextListType TextListType { get; } + public IReadOnlyList Entries { get; } + + public bool TryResolve(long value, out string text) + { + text = null; + if (value < Int32.MinValue || value > Int32.MaxValue) + { + return false; + } + + var intValue = (int)value; + if (_entriesByValue.TryGetValue(intValue, out var entry)) + { + text = entry.Text; + return true; + } + + entry = Entries.FirstOrDefault(item => item.IsRange && intValue >= item.From && intValue <= item.To); + if (entry == null) + { + return false; + } + + text = entry.Text; + return true; + } + } + + public sealed class S7CommPlusTextListCatalog + { + public static readonly S7CommPlusTextListCatalog Empty = new S7CommPlusTextListCatalog(Array.Empty(), Array.Empty()); + + private readonly Dictionary, S7CommPlusTextList> _listsByLanguageAndId; + + public S7CommPlusTextListCatalog(IEnumerable languageIds, IEnumerable textLists) + { + LanguageIds = (languageIds ?? Enumerable.Empty()).Distinct().ToList().AsReadOnly(); + TextLists = (textLists ?? Enumerable.Empty()).ToList().AsReadOnly(); + _listsByLanguageAndId = TextLists + .GroupBy(list => Tuple.Create(list.LanguageId, list.ListId)) + .ToDictionary(group => group.Key, group => group.First()); + } + + public IReadOnlyList LanguageIds { get; } + public IReadOnlyList TextLists { get; } + + public string ResolveText(string textListName, long value, int languageId) + { + return TryResolve(textListName, value, languageId, out var text) ? text : null; + } + + public bool TryResolve(string textListName, long value, int languageId, out string text) + { + text = null; + if (!TryParseTextListId(textListName, out var listId, out var suffix)) + { + return false; + } + + if (TryResolve(listId, value, languageId, out text)) + { + return true; + } + + // Some CPU system-diagnostic placeholders use the TIA display name (for example 7W) + // while the runtime table stores the previous numeric id. + if (StringComparer.OrdinalIgnoreCase.Equals(suffix, "W") && listId > 0) + { + return TryResolve(listId - 1, value, languageId, out text); + } + + return false; + } + + public bool TryResolve(int listId, long value, int languageId, out string text) + { + text = null; + if (TryResolveInLanguage(listId, value, languageId, out text)) + { + return true; + } + + if (languageId != 0 && TryResolveInLanguage(listId, value, 0, out text)) + { + return true; + } + + return false; + } + + private bool TryResolveInLanguage(int listId, long value, int languageId, out string text) + { + text = null; + if (!_listsByLanguageAndId.TryGetValue(Tuple.Create(languageId, listId), out var list)) + { + return false; + } + + return list.TryResolve(value, out text); + } + + private static bool TryParseTextListId(string textListName, out int listId, out string suffix) + { + listId = 0; + suffix = String.Empty; + if (String.IsNullOrWhiteSpace(textListName)) + { + return false; + } + + var pos = 0; + while (pos < textListName.Length && Char.IsDigit(textListName[pos])) + { + pos++; + } + + if (pos == 0) + { + return false; + } + + suffix = textListName.Substring(pos); + return Int32.TryParse(textListName.Substring(0, pos), NumberStyles.None, CultureInfo.InvariantCulture, out listId); + } + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusTisWatch.cs b/src/S7CommPlusDriver/S7CommPlusTisWatch.cs new file mode 100644 index 0000000..0b8d645 --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusTisWatch.cs @@ -0,0 +1,309 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Buffers.Binary; + +namespace S7CommPlusDriver +{ + public sealed class S7CommPlusTisWatchRequest + { + public byte[] RequestBlob { get; set; } = Array.Empty(); + public byte[] TriggerBlob { get; set; } = Array.Empty(); + public string JobName { get; set; } = "S7pDriver_TisWatchJob"; + public S7CommPlusTisResultModel ResultModel { get; set; } = new S7CommPlusTisResultModel(); + public string LastLifecycleStage { get; internal set; } = ""; + + internal S7CommPlusTisWatchRequest Clone() + { + return new S7CommPlusTisWatchRequest + { + RequestBlob = (byte[])(RequestBlob ?? Array.Empty()).Clone(), + TriggerBlob = (byte[])(TriggerBlob ?? Array.Empty()).Clone(), + JobName = String.IsNullOrWhiteSpace(JobName) ? "S7pDriver_TisWatchJob" : JobName, + ResultModel = ResultModel?.Clone() ?? new S7CommPlusTisResultModel(), + LastLifecycleStage = LastLifecycleStage ?? "" + }; + } + + internal void Validate() + { + if (RequestBlob == null || RequestBlob.Length == 0) + { + throw new ArgumentException("A TIS watch request blob (2693) is required.", nameof(RequestBlob)); + } + if (TriggerBlob == null || TriggerBlob.Length == 0) + { + throw new ArgumentException("A TIS watch trigger blob (2694) is required.", nameof(TriggerBlob)); + } + } + } + + public sealed class S7CommPlusTisWatchPointSpec + { + public int Sac { get; set; } + public string NetworkId { get; set; } = ""; + public string Uid { get; set; } = ""; + public string Pin { get; set; } = ""; + public bool IncludeRlo { get; set; } = true; + public List Values { get; } = new List(); + } + + public sealed class S7CommPlusTisWatchValueSpec + { + public string NetworkId { get; set; } = ""; + public string Uid { get; set; } = ""; + public string Pin { get; set; } = ""; + public string DataType { get; set; } = ""; + public byte[] DataAddressBlob { get; set; } = Array.Empty(); + public int ByteLength { get; set; } = 1; + public int Alignment { get; set; } = 1; + public bool NeedsValidCount { get; set; } + public bool InvertBool { get; set; } + public int Sac { get; set; } = -1; + } + + public sealed class S7CommPlusTisResultModel + { + public List WatchPoints { get; } = new List(); + + internal S7CommPlusTisResultModel Clone() + { + var clone = new S7CommPlusTisResultModel(); + foreach (var watchPoint in WatchPoints) + { + clone.WatchPoints.Add(watchPoint.Clone()); + } + return clone; + } + } + + public sealed class S7CommPlusTisWatchPointModel + { + public string NetworkId { get; set; } = ""; + public string Uid { get; set; } = ""; + public string Pin { get; set; } = ""; + public int RloOffset { get; set; } = -1; + public List Values { get; } = new List(); + + internal S7CommPlusTisWatchPointModel Clone() + { + var clone = new S7CommPlusTisWatchPointModel + { + NetworkId = NetworkId ?? "", + Uid = Uid ?? "", + Pin = Pin ?? "", + RloOffset = RloOffset + }; + foreach (var value in Values) + { + clone.Values.Add(value.Clone()); + } + return clone; + } + } + + public sealed class S7CommPlusTisValueModel + { + public string NetworkId { get; set; } = ""; + public string Uid { get; set; } = ""; + public string Pin { get; set; } = ""; + public int ValueOffset { get; set; } = -1; + public int ByteLength { get; set; } = 1; + public string DataType { get; set; } = ""; + public int ValidityOffset { get; set; } = -1; + public int ValidCountOffset { get; set; } = -1; + public bool InvertBool { get; set; } + + internal S7CommPlusTisValueModel Clone() + { + return new S7CommPlusTisValueModel + { + NetworkId = NetworkId ?? "", + Uid = Uid ?? "", + Pin = Pin ?? "", + ValueOffset = ValueOffset, + ByteLength = ByteLength, + DataType = DataType ?? "", + ValidityOffset = ValidityOffset, + ValidCountOffset = ValidCountOffset, + InvertBool = InvertBool + }; + } + } + + public sealed class S7CommPlusTisWatchNotificationEventArgs : EventArgs + { + public S7CommPlusTisWatchNotificationEventArgs(S7CommPlusTisWatchNotification notification) + { + Notification = notification ?? throw new ArgumentNullException(nameof(notification)); + } + + public S7CommPlusTisWatchNotification Notification { get; } + } + + public sealed class S7CommPlusTisWatchNotification + { + public S7CommPlusTisWatchNotification(DateTime timestamp, uint sequenceNumber, byte creditTick, bool? jobEnabled, byte? notificationCredit, byte[] rawResult, IReadOnlyList watchPoints) + { + Timestamp = timestamp; + SequenceNumber = sequenceNumber; + CreditTick = creditTick; + JobEnabled = jobEnabled; + NotificationCredit = notificationCredit; + RawResult = rawResult ?? Array.Empty(); + WatchPoints = watchPoints ?? Array.Empty(); + } + + public DateTime Timestamp { get; } + public uint SequenceNumber { get; } + public byte CreditTick { get; } + public bool? JobEnabled { get; } + public byte? NotificationCredit { get; } + public byte[] RawResult { get; } + public IReadOnlyList WatchPoints { get; } + } + + public sealed class S7CommPlusTisWatchPointResult + { + public S7CommPlusTisWatchPointResult(string networkId, string uid, string pin, bool? rlo, uint rawRloWord, uint executionCount, IReadOnlyList values) + { + NetworkId = networkId ?? ""; + Uid = uid ?? ""; + Pin = pin ?? ""; + Rlo = rlo; + RawRloWord = rawRloWord; + ExecutionCount = executionCount; + Values = values ?? Array.Empty(); + } + + public string NetworkId { get; } + public string Uid { get; } + public string Pin { get; } + public bool? Rlo { get; } + public uint RawRloWord { get; } + public uint ExecutionCount { get; } + public IReadOnlyList Values { get; } + } + + public sealed class S7CommPlusTisValueResult + { + public S7CommPlusTisValueResult(string networkId, string uid, string pin, string dataType, byte[] rawValue, bool? boolValue, byte? validity, ushort? validCount) + { + NetworkId = networkId ?? ""; + Uid = uid ?? ""; + Pin = pin ?? ""; + DataType = dataType ?? ""; + RawValue = rawValue ?? Array.Empty(); + BoolValue = boolValue; + Validity = validity; + ValidCount = validCount; + } + + public string NetworkId { get; } + public string Uid { get; } + public string Pin { get; } + public string DataType { get; } + public byte[] RawValue { get; } + public bool? BoolValue { get; } + public byte? Validity { get; } + public ushort? ValidCount { get; } + public string DisplayValue => FormatDisplayValue(); + + private string FormatDisplayValue() + { + if (BoolValue.HasValue) + return BoolValue.Value ? "TRUE" : "FALSE"; + + var type = ExtractTypeName(DataType); + if (RawValue.Length == 0) + return ""; + + if (type.Equals("LDT", StringComparison.OrdinalIgnoreCase) && RawValue.Length >= 8) + return FormatLdt(ReadUInt64BigEndian(RawValue.AsSpan(0, 8))); + + if (type.Equals("Int", StringComparison.OrdinalIgnoreCase) && RawValue.Length >= 2) + return BinaryPrimitives.ReadInt16BigEndian(RawValue.AsSpan(0, 2)).ToString(CultureInfo.InvariantCulture); + if (type.Equals("UInt", StringComparison.OrdinalIgnoreCase) && RawValue.Length >= 2) + return BinaryPrimitives.ReadUInt16BigEndian(RawValue.AsSpan(0, 2)).ToString(CultureInfo.InvariantCulture); + if (type.Equals("DInt", StringComparison.OrdinalIgnoreCase) && RawValue.Length >= 4) + return BinaryPrimitives.ReadInt32BigEndian(RawValue.AsSpan(0, 4)).ToString(CultureInfo.InvariantCulture); + if (type.Equals("UDInt", StringComparison.OrdinalIgnoreCase) && RawValue.Length >= 4) + return BinaryPrimitives.ReadUInt32BigEndian(RawValue.AsSpan(0, 4)).ToString(CultureInfo.InvariantCulture); + if (type.Equals("LInt", StringComparison.OrdinalIgnoreCase) && RawValue.Length >= 8) + return BinaryPrimitives.ReadInt64BigEndian(RawValue.AsSpan(0, 8)).ToString(CultureInfo.InvariantCulture); + if (type.Equals("ULInt", StringComparison.OrdinalIgnoreCase) && RawValue.Length >= 8) + return BinaryPrimitives.ReadUInt64BigEndian(RawValue.AsSpan(0, 8)).ToString(CultureInfo.InvariantCulture); + if ((type.Equals("Byte", StringComparison.OrdinalIgnoreCase) || type.Equals("USInt", StringComparison.OrdinalIgnoreCase)) && RawValue.Length >= 1) + return RawValue[0].ToString(CultureInfo.InvariantCulture); + if (type.Equals("SInt", StringComparison.OrdinalIgnoreCase) && RawValue.Length >= 1) + return ((sbyte)RawValue[0]).ToString(CultureInfo.InvariantCulture); + + return BitConverter.ToString(RawValue); + } + + private static string ExtractTypeName(string type) + { + if (String.IsNullOrWhiteSpace(type)) + return ""; + + var decoded = type.Replace(""", "\"", StringComparison.OrdinalIgnoreCase); + var end = decoded.LastIndexOf('"'); + if (end >= 0 && end + 1 < decoded.Length) + { + var trailing = decoded.Substring(end + 1).Trim(' ', '}'); + if (!String.IsNullOrWhiteSpace(trailing)) + return trailing; + } + + if (end > 0) + { + var start = decoded.LastIndexOf('"', end - 1); + if (start >= 0 && end > start) + return decoded.Substring(start + 1, end - start - 1); + } + + var trimmed = decoded.Trim('{', '}', ' '); + var split = trimmed.Split(new[] { ' ', '.', ':' }, StringSplitOptions.RemoveEmptyEntries); + return split.Length == 0 ? trimmed : split[split.Length - 1]; + } + + private static ulong ReadUInt64BigEndian(ReadOnlySpan value) => + BinaryPrimitives.ReadUInt64BigEndian(value); + + private static string FormatLdt(ulong nanosecondsSinceUnixEpoch) + { + try + { + var ticks = checked((long)(nanosecondsSinceUnixEpoch / 100UL)); + var date = DateTimeOffset.UnixEpoch.AddTicks(ticks); + var remainder = nanosecondsSinceUnixEpoch % 1_000_000_000UL; + return remainder == 0 + ? date.ToString("LDT#yyyy-MM-dd-HH:mm:ss", CultureInfo.InvariantCulture) + : date.ToString("LDT#yyyy-MM-dd-HH:mm:ss", CultureInfo.InvariantCulture) + "." + remainder.ToString("D9", CultureInfo.InvariantCulture).TrimEnd('0'); + } + catch + { + return nanosecondsSinceUnixEpoch.ToString(CultureInfo.InvariantCulture); + } + } + } + + public sealed class S7CommPlusTisWatchSubscription : S7CommPlusSubscription + { + internal S7CommPlusTisWatchSubscription(S7CommPlusTisResultModel resultModel) + { + ResultModel = resultModel ?? new S7CommPlusTisResultModel(); + } + + public S7CommPlusTisResultModel ResultModel { get; } + public event EventHandler NotificationReceived; + + internal void Publish(S7CommPlusTisWatchNotification notification) + { + if (notification != null) + { + NotificationReceived?.Invoke(this, new S7CommPlusTisWatchNotificationEventArgs(notification)); + } + } + } +} diff --git a/src/S7CommPlusDriver/S7CommPlusTlsBackend.cs b/src/S7CommPlusDriver/S7CommPlusTlsBackend.cs new file mode 100644 index 0000000..575d2c0 --- /dev/null +++ b/src/S7CommPlusDriver/S7CommPlusTlsBackend.cs @@ -0,0 +1,8 @@ +namespace S7CommPlusDriver +{ + public enum S7CommPlusTlsBackend + { + OpenSsl = 0, + BouncyCastle = 1 + } +} diff --git a/src/S7CommPlusDriver/Services/AlarmData.cs b/src/S7CommPlusDriver/Services/AlarmData.cs new file mode 100644 index 0000000..54c711d --- /dev/null +++ b/src/S7CommPlusDriver/Services/AlarmData.cs @@ -0,0 +1,57 @@ +using S7CommPlusDriver.Alarming; +using System; +using System.IO; + +namespace S7CommPlusDriver +{ + internal sealed class AlarmData + { + public AlarmData(uint relationid) + { + RelationId = relationid; + } + + public uint RelationId; + public S7CommPlusConfiguredAlarmInfo MultipleStai; + public S7CommPlusAlarmTexts AlText = new S7CommPlusAlarmTexts(); + + public ulong GetCpuAlarmId() + { + return ((ulong)RelationId << 32) | ((ulong)MultipleStai.Alid << 16); + } + + public int Deserialize(Stream buffer) + { + var ret = 0; + MultipleStai = new S7CommPlusConfiguredAlarmInfo(); + ret += MultipleStai.Deserialize(buffer); + return ret; + } + + public override string ToString() + { + var s = string.Empty; + s += "" + Environment.NewLine; + s += "" + GetCpuAlarmId().ToString() + "" + Environment.NewLine; + s += "" + RelationId.ToString() + Environment.NewLine + "" + Environment.NewLine; + s += "" + Environment.NewLine + MultipleStai.ToString() + "" + Environment.NewLine; + s += "" + Environment.NewLine; + s += "" + AlText.Infotext + "" + Environment.NewLine; + s += "" + AlText.AlarmText + "" + Environment.NewLine; + s += "" + AlText.AdditionalText1 + "" + Environment.NewLine; + s += "" + AlText.AdditionalText2 + "" + Environment.NewLine; + s += "" + AlText.AdditionalText3 + "" + Environment.NewLine; + s += "" + AlText.AdditionalText4 + "" + Environment.NewLine; + s += "" + AlText.AdditionalText5 + "" + Environment.NewLine; + s += "" + AlText.AdditionalText6 + "" + Environment.NewLine; + s += "" + AlText.AdditionalText7 + "" + Environment.NewLine; + s += "" + AlText.AdditionalText8 + "" + Environment.NewLine; + s += "" + AlText.AdditionalText9 + "" + Environment.NewLine; + s += "" + AlText.UnknownValue1.ToString() + "" + Environment.NewLine; + s += "" + AlText.UnknownValue2.ToString() + "" + Environment.NewLine; + s += "" + Environment.NewLine; + s += "" + Environment.NewLine; + return s; + } + } +} diff --git a/src/S7CommPlusDriver/Alarming/BrowseAlarms.cs b/src/S7CommPlusDriver/Services/S7CommPlusAlarmBrowseService.cs similarity index 60% rename from src/S7CommPlusDriver/Alarming/BrowseAlarms.cs rename to src/S7CommPlusDriver/Services/S7CommPlusAlarmBrowseService.cs index a7f96b5..f67c745 100644 --- a/src/S7CommPlusDriver/Alarming/BrowseAlarms.cs +++ b/src/S7CommPlusDriver/Services/S7CommPlusAlarmBrowseService.cs @@ -1,7 +1,7 @@ -#region License +#region License /****************************************************************************** * S7CommPlusDriver - * + * * Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de * * This file is part of S7CommPlusDriver. @@ -19,21 +19,29 @@ using System.IO; using System.Linq; using S7CommPlusDriver.Alarming; +using S7CommPlusDriver.Internal; namespace S7CommPlusDriver { - public partial class S7CommPlusConnection + internal sealed class S7CommPlusAlarmBrowseService { + private readonly S7CommPlusProtocolRequests _requests; + + public S7CommPlusAlarmBrowseService(IS7CommPlusProtocolSession session) + { + _requests = new S7CommPlusProtocolRequests(session); + } + /// /// Explores the AS and the User program alarms, gets the corresponding texts usind the language id (e.g.1031 = de-DE) - /// + /// /// Call example: /// CultureInfo ci = new CultureInfo("de-DE"); /// Dictionary Alarms = new Dictionary(); /// conn.ExploreASAlarms(ref Alarms, ci.LCID); /// foreach (var al in Alarms) /// { - /// Console.WriteLine(al.Value.ToString()); + /// System.Diagnostics.Trace.WriteLine(al.Value.ToString()); /// } /// /// Dictionary where the results are written to. Key is used as address. @@ -44,26 +52,7 @@ public int ExploreASAlarms(ref Dictionary Alarms, int language int res; #region Explore all other than Alarm AP (AnwenderProgramAlarme) - var exploreReq = new ExploreRequest(ProtocolVersion.V2); - exploreReq.ExploreId = 0x8a7e0000; // ASAlarms.0 - exploreReq.ExploreRequestId = Ids.None; - exploreReq.ExploreChildsRecursive = 1; - exploreReq.ExploreParents = 0; - - res = SendS7plusFunctionObject(exploreReq); - if (res != 0) - { - return res; - } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } - - var exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); - res = checkResponseWithIntegrity(exploreReq, exploreRes); + res = _requests.Explore(0x8a7e0000, null, out var exploreRes); // ASAlarms.0 if (res != 0) { return res; @@ -82,7 +71,7 @@ public int ExploreASAlarms(ref Dictionary Alarms, int language var dict = ((ValueBlobSparseArray)stais).GetValue(); foreach (var entry in dict) { - var alarm = new AlarmData(0x8a7e0000); // TODO: Get this from parent object? + var alarm = new AlarmData(0x8a7e0000); // ASAlarms.0 relation. Stream buffer = new MemoryStream(entry.Value.value); alarm.Deserialize(buffer); Alarms.Add(alarm.GetCpuAlarmId(), alarm); @@ -90,50 +79,28 @@ public int ExploreASAlarms(ref Dictionary Alarms, int language } else { - Console.WriteLine("ExploreASAlarms(): stais is not ValueBlobSparseArray"); + System.Diagnostics.Trace.WriteLine("ExploreASAlarms(): stais is not ValueBlobSparseArray"); } } else { - Console.WriteLine("ExploreASAlarms(): stais = null"); + System.Diagnostics.Trace.WriteLine("ExploreASAlarms(): stais = null"); } } else { - Console.WriteLine("ExploreASAlarms(): staiclass = null"); + System.Diagnostics.Trace.WriteLine("ExploreASAlarms(): staiclass = null"); } #endregion #region Explore Alarm AP - exploreReq = new ExploreRequest(ProtocolVersion.V2); - exploreReq.ExploreId = Ids.NativeObjects_thePLCProgram_Rid; - exploreReq.ExploreRequestId = Ids.None; - exploreReq.ExploreChildsRecursive = 1; - exploreReq.ExploreParents = 0; - - // Add the requestes attributes - exploreReq.AddressList.Add(Ids.ObjectVariableTypeParentObject); - exploreReq.AddressList.Add(Ids.MultipleSTAI_STAIs); - - res = SendS7plusFunctionObject(exploreReq); + // Add the requested attributes. + var alarmProgramAttributes = new uint[] { Ids.ObjectVariableTypeParentObject, Ids.MultipleSTAI_STAIs }; + res = _requests.Explore(Ids.NativeObjects_thePLCProgram_Rid, alarmProgramAttributes, out exploreRes); if (res != 0) { return res; } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } - - exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); - if ((exploreRes == null) || - (exploreRes.SequenceNumber != exploreReq.SequenceNumber) || - (exploreRes.ReturnValue != 0)) - { - return S7Consts.errIsoInvalidPDU; - } // All objects which have Alarm AP inside, have a sub-Object with ID 7854 = MultipleSTAI.Class_Rid var obj = exploreRes.Objects.First(o => o.ClassId == Ids.PLCProgram_Class_Rid); @@ -160,12 +127,12 @@ public int ExploreASAlarms(ref Dictionary Alarms, int language } else { - Console.WriteLine("ExploreASAlarms(): stais is not ValueBlobSparseArray"); + System.Diagnostics.Trace.WriteLine("ExploreASAlarms(): stais is not ValueBlobSparseArray"); } } else { - Console.WriteLine("ExploreASAlarms(): stais = null"); + System.Diagnostics.Trace.WriteLine("ExploreASAlarms(): stais = null"); } } } @@ -173,42 +140,31 @@ public int ExploreASAlarms(ref Dictionary Alarms, int language #region Explore AlarmTextLists - exploreReq = new ExploreRequest(ProtocolVersion.V2); - exploreReq.ExploreId = 0x8a360000 + (ushort)languageId; // There may be several languages, add language ID (e.g. 1031 = german / de-DE) - exploreReq.ExploreRequestId = Ids.None; - exploreReq.ExploreChildsRecursive = 0; - exploreReq.ExploreParents = 0; - - res = SendS7plusFunctionObject(exploreReq); + var textLibraryRelationId = 0x8a360000 + (ushort)languageId; // There may be several languages, add language ID (e.g. 1031 = german / de-DE) + res = _requests.Explore(textLibraryRelationId, null, out exploreRes, exploreChildsRecursive: 0); if (res != 0) { return res; } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) + + // TextLibraryOffsetArea is an array[3] of Blob, which contains offset information. + // [0] contains informations of how to get infos from [1], which has infos about offsets in [2]. + // [2] contains the resulting offsets for the strings in TextLibraryStringArea. + var textlib = exploreRes.Objects.FirstOrDefault(o => o.ClassId == Ids.TextLibraryClassRID); + if (textlib == null || + textlib.GetAttribute(Ids.TextLibraryOffsetArea) is not ValueBlobArray offsetArea || + textlib.GetAttribute(Ids.TextLibraryStringArea) is not ValueBlob stringArea) { - return m_LastError; + return S7Consts.errIsoInvalidPDU; } - exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); - if ((exploreRes == null) || - (exploreRes.SequenceNumber != exploreReq.SequenceNumber) || - (exploreRes.ReturnValue != 0)) + var tloa = offsetArea.GetValue(); + var tlsa = stringArea.GetValue(); + if (tloa.Length < 3 || tlsa == null) { return S7Consts.errIsoInvalidPDU; } - // TextLibraryOffsetArea is an array[3] of Blob, which contains offset information. - // [0] contains informations of how to get infos from [1], which has infos about offsets in [2]. - // [2] contains the resulting offsets for the strings in TextLibraryStringArea. - // TODO: Check if all fields are present, or use try/catch? - ValueBlob[] tloa = null; - byte[] tlsa = null; - var textlib = exploreRes.Objects.First(o => o.ClassId == Ids.TextLibraryClassRID); - tloa = ((ValueBlobArray)textlib.GetAttribute(Ids.TextLibraryOffsetArea)).GetValue(); - tlsa = ((ValueBlob)textlib.GetAttribute(Ids.TextLibraryStringArea)).GetValue(); - var tloa_1 = tloa[0].GetValue(); var tloa_2 = tloa[1].GetValue(); var tloa_3 = tloa[2].GetValue(); @@ -335,59 +291,46 @@ private void GetTexts(byte[] tloa_1, byte[] tloa_2, byte[] tloa_3, byte[] tlsa, /// /// Reads the active program alarms from the Plc (single poll). - /// + /// /// Call example: /// CultureInfo ci = new CultureInfo("de-DE"); - /// var alarmList = new List(); + /// var alarmList = new List(); /// conn.GetActiveAlarms(out alarmList, ci.LCID); /// foreach (var a in alarmList) /// { - /// Console.WriteLine(a.ToString()); + /// System.Diagnostics.Trace.WriteLine(a.ToString()); /// } /// /// Contains the alarms, empty if there is no active alarm /// Language id for retrieving the text entries, use language code e.g. 1031 for german /// 0 on success - public int GetActiveAlarms(out List alarmList, int languageId) + public int GetActiveAlarms(out List alarmList, int languageId, Func textListResolver) { int res; - alarmList = new List(); + alarmList = new List(); var exploreReq = new ExploreRequest(ProtocolVersion.V2); - exploreReq.ExploreId = Ids.NativeObjects_theAlarmSubsystem_Rid; - exploreReq.ExploreRequestId = Ids.AlarmSubsystem_itsUpdateRelevantDAI; + exploreReq.ExploreId = Ids.AlarmSubSystem; + exploreReq.ExploreRequestId = Ids.AlarmSubSystem_Update_DAI; exploreReq.ExploreChildsRecursive = 1; exploreReq.ExploreParents = 0; // Add the requestes attributes. // Request the same attributes we get from an alarm notification, so we can reuse other methods. - exploreReq.AddressList.Add(Ids.DAI_CPUAlarmID); - exploreReq.AddressList.Add(Ids.DAI_AllStatesInfo); - exploreReq.AddressList.Add(Ids.DAI_AlarmDomain); - exploreReq.AddressList.Add(Ids.DAI_Coming); - exploreReq.AddressList.Add(Ids.DAI_Going); - exploreReq.AddressList.Add(Ids.DAI_MessageType); - exploreReq.AddressList.Add(Ids.DAI_HmiInfo); + exploreReq.AddressList.Add(Ids.DynamicAlarmInstance_DAI_AlarmId); + exploreReq.AddressList.Add(Ids.DynamicAlarmInstance_DAI_AllStateInformation); + exploreReq.AddressList.Add(Ids.DynamicAlarmInstance_DAI_AlarmDomain); + exploreReq.AddressList.Add(Ids.DynamicAlarmInstance_DAI_Coming); + exploreReq.AddressList.Add(Ids.DynamicAlarmInstance_DAI_Going); + exploreReq.AddressList.Add(Ids.DynamicAlarmInstance_DAI_Messagetype); + exploreReq.AddressList.Add(Ids.DynamicAlarmInstance_DAI_HMIInformation); // Extra ones which we only need for compatibility with notification. exploreReq.AddressList.Add(Ids.ObjectVariableTypeName); - exploreReq.AddressList.Add(Ids.DAI_SequenceCounter); - exploreReq.AddressList.Add(Ids.DAI_AlarmTexts_Rid); - - res = SendS7plusFunctionObject(exploreReq); - if (res != 0) - { - return res; - } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - return m_LastError; - } + exploreReq.AddressList.Add(Ids.DynamicAlarmInstance_DAI_SequenceCounter); + exploreReq.AddressList.Add(Ids.DynamicAlarmInstance_DAI_Alarmtext); - var exploreRes = ExploreResponse.DeserializeFromPdu(m_ReceivedPDU, true); - res = checkResponseWithIntegrity(exploreReq, exploreRes); + res = _requests.SendExplore(exploreReq, out var exploreRes); if (res != 0) { return res; @@ -395,62 +338,11 @@ public int GetActiveAlarms(out List alarmList, int languageId) foreach (var obj in exploreRes.Objects) { - alarmList.Add(AlarmsDai.FromNotificationObject(obj, languageId)); + alarmList.Add(S7CommPlusAlarm.FromNotificationObject(obj, languageId, textListResolver)); } return 0; } } - public class AlarmData - { - public AlarmData(uint relationid) - { - RelationId = relationid; - } - - public ulong GetCpuAlarmId() - { - return ((ulong)(RelationId) << 32) | ((ulong)(MultipleStai.Alid) << 16); - } - - public uint RelationId; - - public AlarmsMultipleStai MultipleStai; - public AlarmsAlarmTexts AlText = new AlarmsAlarmTexts(); - - public int Deserialize(Stream buffer) - { - int ret = 0; - MultipleStai = new AlarmsMultipleStai(); - ret += MultipleStai.Deserialize(buffer); - return ret; - } - - public override string ToString() - { - string s = ""; - s += "" + Environment.NewLine; - s += "" + GetCpuAlarmId().ToString() + "" + Environment.NewLine; - s += "" + RelationId.ToString() + Environment.NewLine +"" + Environment.NewLine; - s += "" + Environment.NewLine + MultipleStai.ToString() + "" + Environment.NewLine; - s += "" + Environment.NewLine; - s += "" + AlText.Infotext + "" + Environment.NewLine; - s += "" + AlText.AlarmText + "" + Environment.NewLine; - s += "" + AlText.AdditionalText1 + "" + Environment.NewLine; - s += "" + AlText.AdditionalText2 + "" + Environment.NewLine; - s += "" + AlText.AdditionalText3 + "" + Environment.NewLine; - s += "" + AlText.AdditionalText4 + "" + Environment.NewLine; - s += "" + AlText.AdditionalText5 + "" + Environment.NewLine; - s += "" + AlText.AdditionalText6 + "" + Environment.NewLine; - s += "" + AlText.AdditionalText7 + "" + Environment.NewLine; - s += "" + AlText.AdditionalText8 + "" + Environment.NewLine; - s += "" + AlText.AdditionalText9 + "" + Environment.NewLine; - s += "" + AlText.UnknownValue1.ToString() + "" + Environment.NewLine; - s += "" + AlText.UnknownValue2.ToString() + "" + Environment.NewLine; - s += "" + Environment.NewLine; - s += "" + Environment.NewLine; - return s; - } - } } diff --git a/src/S7CommPlusDriver/Services/S7CommPlusAlarmSubscriptionService.cs b/src/S7CommPlusDriver/Services/S7CommPlusAlarmSubscriptionService.cs new file mode 100644 index 0000000..7e5bd5c --- /dev/null +++ b/src/S7CommPlusDriver/Services/S7CommPlusAlarmSubscriptionService.cs @@ -0,0 +1,175 @@ +#region License +/****************************************************************************** + * S7CommPlusDriver + * + * Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de + * + * This file is part of S7CommPlusDriver. + * + * S7CommPlusDriver is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + /****************************************************************************/ +#endregion + +using S7CommPlusDriver.Alarming; +using S7CommPlusDriver.Internal; +using System; +using System.Collections.Generic; + +namespace S7CommPlusDriver +{ + internal sealed class S7CommPlusAlarmSubscriptionService + { + private readonly IS7CommPlusProtocolSession _session; + private readonly S7CommPlusProtocolRequests _requests; + + public S7CommPlusAlarmSubscriptionService(IS7CommPlusProtocolSession session) + { + _session = session; + _requests = new S7CommPlusProtocolRequests(session); + } + + // Example code for testing: + // CultureInfo ci = new CultureInfo("en-US"); + // client.SubscribeAlarmsAsync(...); + // await subscription.Completion; + // await subscription.DisposeAsync(); + + uint m_AlarmSubscriptionRelationId = S7CommPlusProtocolConstants.SubscriptionRelationIdStart; + uint m_AlarmSubscriptionRefRelationId = S7CommPlusProtocolConstants.AlarmSubscriptionRefRelationIdStart; + private readonly Dictionary _subscriptions = new Dictionary(); + + private sealed class AlarmSubscriptionState + { + public short NextCreditLimit { get; set; } + } + + public int Create(uint[] languageIds) + { + return Create(languageIds, S7CommPlusProtocolConstants.DefaultSubscriptionCreditLimit, out _); + } + + public int Create(uint[] languageIds, short initialCreditLimit, out uint subscriptionObjectId) + { + subscriptionObjectId = 0; + int res; + languageIds ??= Array.Empty(); + var state = new AlarmSubscriptionState { NextCreditLimit = initialCreditLimit }; + var subscriptionRelationId = m_AlarmSubscriptionRelationId++; + var subscriptionRefRelationId = m_AlarmSubscriptionRefRelationId++; + PObject subsobj = new PObject(); + subsobj.ClassId = Ids.ClassSubscription; + subsobj.RelationId = subscriptionRelationId; + subsobj.AddAttribute(Ids.ObjectVariableTypeName, new ValueWString("Subscription_" + subscriptionRelationId.ToString())); + subsobj.AddAttribute(Ids.SubscriptionFunctionClassId, new ValueUSInt((byte)SubscriptionFunctionClass.Alarms)); + subsobj.AddAttribute(Ids.SubscriptionMissedSendings, new ValueUInt(0)); + subsobj.AddAttribute(Ids.SubscriptionSubsystemError, new ValueLInt(0)); + subsobj.AddAttribute(Ids.SubscriptionRouteMode, new ValueUSInt((byte)SubscriptionRouteMode.Alarm)); + subsobj.AddAttribute(Ids.SubscriptionActive, new ValueBool(true)); + subsobj.AddAttribute(Ids.SubscriptionReferenceList, new ValueUDIntArray(new uint[3] { S7CommPlusProtocolConstants.AlarmSubscriptionReferenceListHeader, 0, 0 }, S7CommPlusProtocolConstants.ValueAddressArrayFlag)); + subsobj.AddAttribute(Ids.SubscriptionCycleTime, new ValueUDInt(0)); + subsobj.AddAttribute(Ids.SubscriptionDelayTime, new ValueUDInt(0)); + subsobj.AddAttribute(Ids.SubscriptionDisabled, new ValueUSInt(0)); + subsobj.AddAttribute(Ids.SubscriptionCount, new ValueUSInt(0)); + subsobj.AddAttribute(Ids.SubscriptionCreditLimit, new ValueInt(state.NextCreditLimit)); // -1=unlimited, 255 = max + subsobj.AddAttribute(Ids.SubscriptionTicks, new ValueUInt(S7CommPlusProtocolConstants.SubscriptionTicksUnlimited)); + PObject asrefsobj = new PObject(); + asrefsobj.ClassId = Ids.AlarmSubscriptionRef_Class_Rid; + asrefsobj.RelationId = subscriptionRefRelationId; + asrefsobj.AddAttribute(Ids.ObjectVariableTypeName, new ValueWString(S7CommPlusProtocolConstants.AlarmSubscriptionName)); + asrefsobj.AddAttribute(Ids.SubscriptionReferenceMode, new ValueUSInt(S7CommPlusProtocolConstants.AlarmSubscriptionTriggerAndTransmitMode)); + asrefsobj.AddAttribute(Ids.AlarmSubSystem_AlarmDomain, new ValueUIntArray(new ushort[10] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, S7CommPlusProtocolConstants.ValueArrayFlag)); + // Also variant to set explicit the alarm domain as filter, for example: + // {1, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272} + // ASOM calls 7731 AlarmDomain2; observed value 65535 subscribes to all alarm domains. + asrefsobj.AddAttribute(Ids.AlarmSubscr_AlarmDomain2, new ValueUIntArray(new ushort[1] { S7CommPlusProtocolConstants.AlarmDomainAll }, S7CommPlusProtocolConstants.ValueAddressArrayFlag)); + // OPTION: + // Send text informations with the message, we don't need to browse them in advance. + // PLCcom names AID 8181 AlarmSubscr_AlarmTextLangIdentifier; an empty array requests all text languages. + asrefsobj.AddAttribute(Ids.AlarmSubscr_AlarmTextLangIdentifier, new ValueUDIntArray(languageIds, S7CommPlusProtocolConstants.ValueAddressArrayFlag)); // Empty for all languages. Otherwise e.g. 1031 for de-DE or the requested language. + asrefsobj.AddAttribute(Ids.AlarmSubscr_SendAlarmTextIdentifier, new ValueBool(true)); + + asrefsobj.AddRelation(Ids.AlarmSubscriptionRef_itsAlarmSubsystem, Ids.NativeObjects_theAlarmSubsystem_Rid); + subsobj.AddObject(asrefsobj); + // Build the request object + var createObjReq = new CreateObjectRequest(ProtocolVersion.V2, 0, true); + createObjReq.TransportFlags = S7CommPlusProtocolConstants.RequestWithResponseTransportFlags; + createObjReq.RequestId = _session.SessionId2; + createObjReq.RequestValue = new ValueUDInt(0); + createObjReq.SetRequestObject(subsobj); + + res = _requests.CreateObject(createObjReq, out var createObjRes); + if (res != 0) + { + _session.DisconnectTransport(); + return res; + } + + if (createObjRes.ReturnValue == 0) + { + subscriptionObjectId = createObjRes.ObjectIds[0]; + _subscriptions[subscriptionObjectId] = state; + } + else + { + // If creating a subscription fails, the object is still created and should be deleted. + // At least deleting it, gives no error. + System.Diagnostics.Trace.WriteLine(String.Format("AlarmSubscription - Create: Failed with Returnvalue = 0x{0:X8}", createObjRes.ReturnValue)); + res = S7Consts.errCliInvalidParams; + } + + return res; + } + + public int WaitForNotifications(int waitTimeout, out List notifications) + { + return WaitForNotifications(0, waitTimeout, S7CommPlusProtocolConstants.DefaultSubscriptionCreditLimitStep, out notifications); + } + + public int WaitForNotifications(uint subscriptionObjectId, int waitTimeout, short creditLimitStep, out List notifications) + { + notifications = new List(); + if (!_subscriptions.TryGetValue(subscriptionObjectId, out var state)) + { + return S7Consts.errCliInvalidParams; + } + + var result = _requests.WaitNotification(subscriptionObjectId, waitTimeout, out var noti); + if (result != 0) + { + return result; + } + + notifications.Add(noti); + + if (creditLimitStep > 0 && noti.NotificationCreditTick >= state.NextCreditLimit - 1) // Set new limit one tick before it expires, to get a constant flow of data + { + // CreditTick in Notification is only one byte + state.NextCreditLimit = (short)((state.NextCreditLimit + creditLimitStep) % 255); + if (state.NextCreditLimit == 0) + { + state.NextCreditLimit = creditLimitStep; + } + return _requests.SetSubscriptionCreditLimit(subscriptionObjectId, state.NextCreditLimit); + } + + return 0; + } + + public int Delete(uint subscriptionObjectId) + { + int res; + if (subscriptionObjectId == 0) + { + return 0; + } + + _subscriptions.Remove(subscriptionObjectId); + System.Diagnostics.Trace.WriteLine(String.Format("AlarmSubscriptionDelete: Calling DeleteObject for SubscriptionObjectId={0:X8}", subscriptionObjectId)); + res = _session.DeleteObject(subscriptionObjectId); + return res; + } + } +} diff --git a/src/S7CommPlusDriver/Services/S7CommPlusMetadataService.cs b/src/S7CommPlusDriver/Services/S7CommPlusMetadataService.cs new file mode 100644 index 0000000..36fae70 --- /dev/null +++ b/src/S7CommPlusDriver/Services/S7CommPlusMetadataService.cs @@ -0,0 +1,1053 @@ +#region License +/****************************************************************************** + * S7CommPlusDriver + * + * Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de + * + * This file is part of S7CommPlusDriver. + * + * S7CommPlusDriver is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + /****************************************************************************/ +#endregion + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml.Linq; +using S7CommPlusDriver.Internal; + +namespace S7CommPlusDriver +{ + internal sealed class S7CommPlusMetadataService + { + private readonly IS7CommPlusProtocolSession _session; + private readonly S7CommPlusProtocolRequests _requests; + + public S7CommPlusMetadataService(IS7CommPlusProtocolSession session) + { + _session = session; + _requests = new S7CommPlusProtocolRequests(session); + } + + + public int BrowseBlocks(out List exploreData) + { + int res; + Browser vars = new Browser(); + ExploreRequest exploreReq; + ExploreResponse exploreRes; + + #region Read all objects + + exploreData = new List(); + + exploreReq = new ExploreRequest(ProtocolVersion.V2); + exploreReq.ExploreId = Ids.NativeObjects_thePLCProgram_Rid; + exploreReq.ExploreRequestId = Ids.None; + exploreReq.ExploreChildsRecursive = 1; + exploreReq.ExploreParents = 0; + + // We want to know the following attributes + exploreReq.AddressList.Add(Ids.ObjectVariableTypeName); + exploreReq.AddressList.Add(Ids.Block_BlockNumber); + exploreReq.AddressList.Add(Ids.Block_BlockLanguage); + + res = _requests.SendExplore(exploreReq, out exploreRes); + if (res != 0) + { + return res; + } + + #endregion + + #region Evaluate all data blocks that then need to be browsed + + var obj = exploreRes.Objects.First(o => o.ClassId == Ids.PLCProgram_Class_Rid); + foreach (var ob in obj.GetObjects()) + { + switch (ob.ClassId) + { + case Ids.DB_Class_Rid: + case Ids.FB_Class_Rid: + case Ids.FC_Class_Rid: + case Ids.OB_Class_Rid: + case 2637: + case 2639: + case 2640: + case 2641: + case 2642: + case 2643: + case 2644: + case 2645: + case 2646: + case 2647: + case 2648: + case 2649: + case 2650: + case 2651: + case 2652: + case 2653: + case 2654: + case 2655: + case 2656: + case 2657: + case 2658: + case 8440: + UInt32 relid = ob.RelationId; + UInt32 area = (relid >> 16); + UInt32 num = relid & 0xffff; + + var name = (ValueWString)(ob.GetAttribute(Ids.ObjectVariableTypeName)); + var data = new S7CommPlusBlockInfo(); + data.RelationId = relid; + data.Name = name.GetValue(); + data.Number = num; + data.Type = GetBlockTypeFromClassId(ob.ClassId); + + var lang = ((ValueUInt)ob.Attributes[Ids.Block_BlockLanguage]).GetValue(); + data.Language = (S7CommPlusProgrammingLanguage)lang; + exploreData.Add(data); + break; + } + } + + #endregion + + return 0; + } + + public int GetPlcStructureXml(out S7CommPlusPlcStructureSnapshot plcStructure) + { + int res; + Browser vars = new Browser(); + ExploreRequest exploreReq; + ExploreResponse exploreRes; + plcStructure = PlcStructureXmlParser.CreateSnapshot(string.Empty); + + #region Read all objects + + exploreReq = new ExploreRequest(ProtocolVersion.V2); + exploreReq.ExploreId = Ids.Constants | 0x0000ffff; + exploreReq.ExploreRequestId = Ids.None; + exploreReq.ExploreChildsRecursive = 1; + exploreReq.ExploreParents = 1; + + exploreReq.AddressList.Add(Ids.ConstantsGlobal_Symbolics); + + res = _requests.SendExplore(exploreReq, out exploreRes); + if (res != 0) + { + return res; + } + + #endregion + + var attr = exploreRes?.Objects?[0]?.Objects?.First().Value?.Objects?.First().Value?.Attributes?[Ids.ConstantsGlobal_Symbolics] as ValueBlob; + if (attr != null) + { + BlobDecompressor bd3 = new BlobDecompressor(); + var v = attr.GetValue(); + var xml = bd3.decompress(v, 0); + plcStructure = PlcStructureXmlParser.CreateSnapshot(xml); + } + + return 0; + } + + public int GetBlockContent(uint relid, out S7CommPlusClientBlockContent blockContent) + { + int res; + // With requesting DataInterface_InterfaceDescription, whe would be able to get all informations like the access ids and + // datatype informations, that we get from the other browsing method. Needs to be tested which one is more efficient on network traffic or plc load. + // If we keep use browsing for the comments, at least we would be able to read all information in one request. + blockContent = null; + var xmlLineComment = String.Empty; + var xmlComments = new Dictionary(); + var interfaceDescription = String.Empty; + var blockBody = Array.Empty(); + var functionalObjectCode = String.Empty; + var functionalObjectCodeBytes = Array.Empty(); + var functionalObjectDebugInfo = String.Empty; + var internalReferences = Array.Empty(); + var externalReferences = Array.Empty(); + var codeModifiedTimestampBytes = Array.Empty(); + var binaryArtifacts = new Dictionary(); + var onlineMetadata = new Dictionary(); + var networkComments = new Dictionary(); + var networkTitles = new Dictionary(); + string blockName = null; + var lang = S7CommPlusProgrammingLanguage.Undef; + var blockNumber = relid & 0xffff; + var blockType = S7CommPlusBlockType.Unknown; + + var exploreReq = new ExploreRequest(ProtocolVersion.V2); + exploreReq.ExploreId = relid; + exploreReq.ExploreRequestId = Ids.None; + exploreReq.ExploreChildsRecursive = 1; + exploreReq.ExploreParents = 0; + + // We want to know the following attributes + exploreReq.AddressList.Add(Ids.ObjectVariableTypeName); + //exploreReq.AddressList.Add(Ids.Block_BlockNumber); + exploreReq.AddressList.Add(Ids.Block_BlockLanguage); + exploreReq.AddressList.Add(Ids.Block_RuntimeModified); + exploreReq.AddressList.Add(Ids.Block_CRC); + exploreReq.AddressList.Add(Ids.Block_FunctionalSignature); + + exploreReq.AddressList.Add(Ids.ASObjectES_Comment); + exploreReq.AddressList.Add(Ids.DataInterface_LineComments); + exploreReq.AddressList.Add(Ids.DataInterface_InterfaceDescription); + exploreReq.AddressList.Add(Ids.Block_BodyDescription); + exploreReq.AddressList.Add(Ids.FunctionalObject_Code); + exploreReq.AddressList.Add(Ids.FunctionalObject_ParameterModified); + exploreReq.AddressList.Add(Ids.FunctionalObject_InterfaceSignature); + exploreReq.AddressList.Add(Ids.FunctionalObject_NetworkComments); + exploreReq.AddressList.Add(Ids.FunctionalObject_NetworkTitles); + exploreReq.AddressList.Add(Ids.FunctionalObject_DebugInfo); + exploreReq.AddressList.Add(Ids.FunctionalObject_extRefData); + exploreReq.AddressList.Add(Ids.FunctionalObject_intRefData); + + res = _requests.SendExplore(exploreReq, out var exploreRes); + if (res != 0) + { + return res; + } + + foreach (var obj in exploreRes.Objects) + { + blockType = GetBlockTypeFromClassId(obj.ClassId); + + + foreach (var att in obj.Attributes) + { + onlineMetadata[att.Key] = DescribeValue(att.Value); + switch (att.Key) + { + case Ids.ObjectVariableTypeName: + { + blockName = ((ValueWString)att.Value).GetValue(); + break; + } + case Ids.Block_BlockNumber: + { + break; + } + case Ids.Block_BlockLanguage: + { + var l = ((ValueUInt)att.Value).GetValue(); + lang = (S7CommPlusProgrammingLanguage)l; + break; + } + case Ids.Block_RuntimeModified: + { + var timestamp = TryGetUInt64(att.Value); + if (timestamp.HasValue) + codeModifiedTimestampBytes = ToBigEndianBytes(timestamp.Value); + break; + } + case Ids.FunctionalObject_extRefData: + { + var xx = (ValueBlobSparseArray)att.Value; + BlobDecompressor bd3 = new BlobDecompressor(); + var blob_sp3 = xx.GetValue(); + externalReferences = new string[blob_sp3.Count]; + var i = 0; + foreach (var key in blob_sp3.Keys) + { + if (blob_sp3[key].value != null) + externalReferences[i++] = bd3.decompress(blob_sp3[key].value, 4); + } + break; + } + case Ids.FunctionalObject_intRefData: + { + var xx = (ValueBlobSparseArray)att.Value; + BlobDecompressor bd3 = new BlobDecompressor(); + var blob_sp3 = xx.GetValue(); + internalReferences = new string[blob_sp3.Count]; + var i = 0; + foreach (var key in blob_sp3.Keys) + { + if (blob_sp3[key].value != null) + internalReferences[i++] = bd3.decompress(blob_sp3[key].value, 4); + } + break; + } + case Ids.ASObjectES_Comment: + { + var att_comment = (ValueWStringSparseArray)att.Value; + xmlComments = att_comment.GetValue(); + break; + } + case Ids.DataInterface_LineComments: + { + var att_linecomment = (ValueBlobSparseArray)att.Value; + BlobDecompressor bd = new BlobDecompressor(); + var blob_sp = att_linecomment.GetValue(); + // In DBs we get the data with Sparsearray key = 1, in M-Area with key = 2. + // For now, just take the first, don't know where the key ids are for. + foreach (var key in blob_sp.Keys) + { + xmlLineComment = bd.decompress(blob_sp[key].value, 4); // Offset of 4, as we have a header for the zlib dictionary version + break; + } + break; + } + case Ids.FunctionalObject_NetworkComments: + { + networkComments = ReadSparseStrings(att.Value); + break; + } + case Ids.FunctionalObject_NetworkTitles: + { + networkTitles = ReadSparseStrings(att.Value); + break; + } + case Ids.DataInterface_InterfaceDescription: + { + var att_ifsescr = (ValueBlob)att.Value; + BlobDecompressor bd2 = new BlobDecompressor(); + var blob_sp2 = att_ifsescr.GetValue(); + interfaceDescription = bd2.decompress(blob_sp2, 4); // Offset of 4, as we have a header for the zlib dictionary version + break; + } + case Ids.Block_BodyDescription: + { + var xx = (ValueBlobSparseArray)att.Value; + BlobDecompressor bd3 = new BlobDecompressor(); + var blob_sp3 = xx.GetValue(); + blockBody = new string[blob_sp3.Where(x => x.Key < (uint)S7CommPlusBinaryArtifactType.PlcFamily).Count()]; + var i = 0; + foreach (var key in blob_sp3.Keys.OrderBy(x => x)) + { + if (!(key < (uint)S7CommPlusBinaryArtifactType.PlcFamily)) + { + // Binary artifacts are metadata, not source text; keep them out of blockBody. + var binaryArtifactType = (S7CommPlusBinaryArtifactType)key; + if (blob_sp3[key].value != null) + binaryArtifacts[key] = blob_sp3[key].value; + continue; + } + + if (blob_sp3[key].value != null) + { + var code = bd3.decompress(blob_sp3[key].value, 4); + blockBody[i++] = code; + } + } + break; + } + case Ids.FunctionalObject_DebugInfo: + { + functionalObjectDebugInfo = DecompressFirstBlob(att.Value); + break; + } + case Ids.FunctionalObject_Code: + { + functionalObjectCodeBytes = GetFirstBlobBytes(att.Value); + functionalObjectCode = TryDecompressFirstBlob(att.Value); + break; + } + } + } + } + + blockContent = new S7CommPlusClientBlockContent( + relid, + blockName, + lang, + blockNumber, + blockType, + xmlLineComment, + xmlComments, + interfaceDescription, + blockBody, + functionalObjectCode, + functionalObjectDebugInfo, + internalReferences, + externalReferences, + functionalObjectCodeBytes, + codeModifiedTimestampBytes, + binaryArtifacts, + onlineMetadata, + networkComments, + networkTitles); + return 0; + } + + private static S7CommPlusBlockType GetBlockTypeFromClassId(uint classId) + { + return classId switch + { + Ids.DB_Class_Rid => S7CommPlusBlockType.DB, + Ids.FB_Class_Rid => S7CommPlusBlockType.FB, + Ids.FC_Class_Rid => S7CommPlusBlockType.FC, + Ids.UDT_Class_Rid => S7CommPlusBlockType.UDT, + Ids.OB_Class_Rid => S7CommPlusBlockType.OB, + 2637 => S7CommPlusBlockType.OB, // ACCcommunicationOB + 2639 => S7CommPlusBlockType.OB, // CPUredundancyErrorOB + 2640 => S7CommPlusBlockType.OB, // CyclicOB + 2641 => S7CommPlusBlockType.OB, // DiagnosticErrorOB + 2642 => S7CommPlusBlockType.OB, // IOaccessErrorOB + 2643 => S7CommPlusBlockType.OB, // IOredundancyErrorOB + 2644 => S7CommPlusBlockType.OB, // PeripheralAccessErrorOB + 2645 => S7CommPlusBlockType.OB, // ProcessEventOB + 2646 => S7CommPlusBlockType.OB, // ProfileEventOB + 2647 => S7CommPlusBlockType.OB, // ProgramCycleOB (OB1) + 2648 => S7CommPlusBlockType.OB, // ProgrammingErrorOB + 2649 => S7CommPlusBlockType.OB, // PullPlugEventOB + 2650 => S7CommPlusBlockType.OB, // RackStationFailureOB + 2651 => S7CommPlusBlockType.OB, // StartupOB + 2652 => S7CommPlusBlockType.OB, // StatusEventOB + 2653 => S7CommPlusBlockType.OB, // SynchronousCycleOB + 2654 => S7CommPlusBlockType.OB, // TechnologyEventOB + 2655 => S7CommPlusBlockType.OB, // TimeDelayOB + 2656 => S7CommPlusBlockType.OB, // TimeErrorOB + 2657 => S7CommPlusBlockType.OB, // TimeOfDayOB + 2658 => S7CommPlusBlockType.OB, // UpdateEventOB + 8440 => S7CommPlusBlockType.OB, // LookAheadOB + _ => S7CommPlusBlockType.Unknown, + }; + } + + private static Dictionary ReadSparseStrings(PValue value) + { + switch (value) + { + case ValueWStringSparseArray strings: + { + var result = new Dictionary(); + foreach (var item in strings.GetValue()) + { + result[TryReadTextRefId(item.Value) ?? item.Key] = item.Value; + } + + return result; + } + case ValueBlobSparseArray blobs: + { + var result = new Dictionary(); + var decompressor = new BlobDecompressor(); + foreach (var item in blobs.GetValue()) + { + if (item.Value.value != null) + { + var text = decompressor.decompress(item.Value.value, 4); + result[TryReadTextRefId(text) ?? item.Key] = text; + } + } + + return result; + } + default: + return new Dictionary(); + } + } + + private static uint? TryReadTextRefId(string text) + { + if (string.IsNullOrWhiteSpace(text) || !text.TrimStart().StartsWith("<")) + return null; + + try + { + var root = XDocument.Parse(text).Root; + var refId = root?.Attributes() + .FirstOrDefault(x => x.Name.LocalName.Equals("RefID", StringComparison.OrdinalIgnoreCase) || + x.Name.LocalName.Equals("RefId", StringComparison.OrdinalIgnoreCase) || + x.Name.LocalName.Equals("refId", StringComparison.OrdinalIgnoreCase)) + ?.Value; + + return uint.TryParse(refId, out var value) ? value : null; + } + catch + { + return null; + } + } + + private static string DescribeValue(PValue value) + { + if (value == null) + return String.Empty; + + var numeric = TryGetUInt64(value); + if (numeric.HasValue) + return numeric.Value.ToString(); + + return value.ToString(); + } + + private static ulong? TryGetUInt64(PValue value) + { + switch (value) + { + case ValueTimestamp timestamp: + return timestamp.GetValue(); + case ValueULInt ulint: + return ulint.GetValue(); + case ValueUDInt udint: + return udint.GetValue(); + case ValueUInt uintValue: + return uintValue.GetValue(); + case ValueUSInt usint: + return usint.GetValue(); + case ValueLWord lword: + return lword.GetValue(); + case ValueDWord dword: + return dword.GetValue(); + case ValueWord word: + return word.GetValue(); + case ValueByte byteValue: + return byteValue.GetValue(); + default: + return null; + } + } + + private static byte[] ToBigEndianBytes(ulong value) + { + return new[] + { + (byte)(value >> 56), + (byte)(value >> 48), + (byte)(value >> 40), + (byte)(value >> 32), + (byte)(value >> 24), + (byte)(value >> 16), + (byte)(value >> 8), + (byte)value + }; + } + + private static byte[] GetFirstBlobBytes(PValue value) + { + if (value is ValueBlob blob) + { + return blob.GetValue() ?? Array.Empty(); + } + + if (value is ValueBlobSparseArray sparseArray) + { + var sparse = sparseArray.GetValue(); + foreach (var key in sparse.Keys.OrderBy(x => x)) + { + var data = sparse[key].value; + if (data != null && data.Length > 0) + return data; + } + } + + return Array.Empty(); + } + + private static string TryDecompressFirstBlob(PValue value) + { + try + { + return DecompressFirstBlob(value); + } + catch (InvalidDataException) + { + return String.Empty; + } + } + + private static string DecompressFirstBlob(PValue value) + { + var decompressor = new BlobDecompressor(); + if (value is ValueBlob blob) + { + var data = blob.GetValue(); + return data == null || data.Length == 0 ? String.Empty : decompressor.decompress(data, 4); + } + + if (value is ValueBlobSparseArray sparseArray) + { + var sparse = sparseArray.GetValue(); + foreach (var key in sparse.Keys.OrderBy(x => x)) + { + var data = sparse[key].value; + if (data != null && data.Length > 0) + return decompressor.decompress(data, 4); + } + } + + return String.Empty; + } + + public int RunExploreRequest(uint relid, uint[] attributes, out List objects, byte exploreChildsRecursive = 1, byte exploreParents = 0) + { + objects = null; + var res = _requests.Explore(relid, attributes, out var exploreRes, exploreChildsRecursive, exploreParents); + if (res != 0) + { + return res; + } + + objects = exploreRes.Objects; + + return 0; + } + + public int RunGetVarSubstreamedRequest(uint objectId, ushort address, out PValue value) + { + return _requests.GetVarSubstreamed(objectId, address, out value); + } + + public int GetCpuState(out S7CommPlusCpuState cpuState) + { + cpuState = null; + + var res = RunGetVarSubstreamedRequest(Ids.NativeObjects_theCPUexecUnit_Rid, Ids.HWObject_DIS, out var value); + if (res == 0 && + TryGetStructElement(value, Ids.AS_DIS_OperatingState, out var operatingStateValue) && + TryGetInt32(operatingStateValue, out var operatingState)) + { + var displayStateSwitch = ReadOptionalCpuStateSwitch(); + cpuState = new S7CommPlusCpuState(operatingState, MapCpuOperatingState(operatingState), displayStateSwitch, MapCpuStateSwitch(displayStateSwitch)); + return 0; + } + + res = RunGetVarSubstreamedRequest(Ids.NativeObjects_theCPUCommon_Rid, Ids.CPUCommon_OperatingState, out value); + if (res != 0) + { + return res; + } + + if (!TryGetInt32(value, out operatingState)) + { + return S7Consts.errIsoInvalidPDU; + } + + var stateSwitch = ReadOptionalCpuStateSwitch(); + cpuState = new S7CommPlusCpuState(operatingState, MapCpuOperatingState(operatingState), stateSwitch, MapCpuStateSwitch(stateSwitch)); + return 0; + } + + private int? ReadOptionalCpuStateSwitch() + { + var res = RunGetVarSubstreamedRequest(Ids.NativeObjects_theCPUCommon_Rid, Ids.CPUCommon_StateSwitch2, out var value); + if (res != 0) + { + return null; + } + + return TryGetInt32(value, out var stateSwitch) ? stateSwitch : (int?)null; + } + + public int GetCpuCycleTime(out S7CommPlusCpuCycleTime cycleTime) + { + cycleTime = null; + + var configuredMinimum = ReadOptionalCycleTime(Ids.CPUexecUnit_ConfiguredMinScanCycle); + var configuredMaximum = ReadOptionalCycleTime(Ids.CPUexecUnit_ConfiguredMaxScanCycle); + + var res = ReadRequiredCycleTime(Ids.CPUexecUnit_ShortestScanCycle, out var shortest); + if (res != 0) + { + return res; + } + + res = ReadRequiredCycleTime(Ids.CPUexecUnit_CurrentScanCycle, out var current); + if (res != 0) + { + return res; + } + + res = ReadRequiredCycleTime(Ids.CPUexecUnit_LongestScanCycle, out var longest); + if (res != 0) + { + return res; + } + + cycleTime = new S7CommPlusCpuCycleTime(configuredMinimum, configuredMaximum, shortest, current, longest); + return 0; + } + + public int GetCpuMemoryUsage(out S7CommPlusCpuMemoryUsage memoryUsage) + { + memoryUsage = null; + var firstError = 0; + var areas = new List(); + + ReadMemoryArea( + areas, + ref firstError, + "load", + "Load memory", + Ids.CPUCommon_LoadMemoryTotal, + Ids.CPUCommon_LoadMemoryUsed); + ReadMemoryArea( + areas, + ref firstError, + "work", + "Work memory", + Ids.CPUCommon_WorkMemoryTotal, + Ids.CPUCommon_WorkMemoryUsed); + ReadMemoryArea( + areas, + ref firstError, + "work-code", + "Work memory code", + Ids.CPUCommon_WorkMemoryCodeTotal, + Ids.CPUCommon_WorkMemoryCodeUsed); + ReadMemoryArea( + areas, + ref firstError, + "work-data", + "Work memory data", + Ids.CPUCommon_WorkMemoryDataTotal, + Ids.CPUCommon_WorkMemoryDataUsed); + ReadMemoryArea( + areas, + ref firstError, + "retain", + "Retain memory", + Ids.CPUCommon_RetainMemoryTotal, + Ids.CPUCommon_RetainMemoryUsed); + + if (areas.Count == 0) + { + return firstError == 0 ? S7Consts.errIsoInvalidPDU : firstError; + } + + memoryUsage = new S7CommPlusCpuMemoryUsage(areas); + return 0; + } + + private void ReadMemoryArea( + ICollection areas, + ref int firstError, + string key, + string name, + int totalAddress, + int usedAddress) + { + var totalResult = ReadOptionalMemorySize(totalAddress, out var totalBytes); + var usedResult = ReadOptionalMemorySize(usedAddress, out var usedBytes); + if (totalResult != 0 || usedResult != 0 || totalBytes <= 0) + { + if (firstError == 0) + { + firstError = totalResult != 0 ? totalResult : usedResult; + } + return; + } + + areas.Add(new S7CommPlusCpuMemoryArea(key, name, totalBytes, usedBytes)); + } + + private int ReadOptionalMemorySize(int address, out long bytes) + { + bytes = 0; + var res = RunGetVarSubstreamedRequest(Ids.NativeObjects_theCPUCommon_Rid, (ushort)address, out var value); + if (res != 0) + { + return res; + } + + if (!TryGetInt64(value, out bytes)) + { + return S7Consts.errIsoInvalidPDU; + } + + return 0; + } + + private int ReadRequiredCycleTime(int address, out double milliseconds) + { + milliseconds = 0; + var res = RunGetVarSubstreamedRequest(Ids.NativeObjects_theCPUexecUnit_Rid, (ushort)address, out var value); + if (res != 0) + { + return res; + } + + return TryGetMilliseconds(value, out milliseconds) + ? 0 + : S7Consts.errIsoInvalidPDU; + } + + private double? ReadOptionalCycleTime(int address) + { + var res = RunGetVarSubstreamedRequest(Ids.NativeObjects_theCPUexecUnit_Rid, (ushort)address, out var value); + if (res != 0) + { + return null; + } + + return TryGetMilliseconds(value, out var milliseconds) ? milliseconds : (double?)null; + } + + private static bool TryGetStructElement(PValue value, int elementId, out PValue element) + { + element = null; + if (value is not ValueStruct valueStruct) + { + return false; + } + + try + { + element = valueStruct.GetStructElement((uint)elementId); + return true; + } + catch (KeyNotFoundException) + { + return false; + } + } + + private static bool TryGetMilliseconds(PValue value, out double milliseconds) + { + milliseconds = 0; + if (!TryGetDouble(value, out var rawValue)) + { + return false; + } + + milliseconds = IsFloatingPoint(value) ? rawValue : rawValue / 1000.0; + return true; + } + + private static bool TryGetInt32(PValue value, out int result) + { + result = 0; + if (!TryGetDouble(value, out var doubleValue)) + { + return false; + } + + if (doubleValue < Int32.MinValue || doubleValue > Int32.MaxValue) + { + return false; + } + + result = Convert.ToInt32(doubleValue); + return true; + } + + private static bool TryGetInt64(PValue value, out long result) + { + result = 0; + if (!TryGetDouble(value, out var doubleValue)) + { + return false; + } + + if (doubleValue < Int64.MinValue || doubleValue > Int64.MaxValue) + { + return false; + } + + result = Convert.ToInt64(doubleValue); + return true; + } + + private static bool TryGetDouble(PValue value, out double result) + { + switch (value) + { + case ValueUSInt v: + result = v.GetValue(); + return true; + case ValueUInt v: + result = v.GetValue(); + return true; + case ValueUDInt v: + result = v.GetValue(); + return true; + case ValueULInt v: + result = v.GetValue(); + return true; + case ValueSInt v: + result = v.GetValue(); + return true; + case ValueInt v: + result = v.GetValue(); + return true; + case ValueDInt v: + result = v.GetValue(); + return true; + case ValueLInt v: + result = v.GetValue(); + return true; + case ValueByte v: + result = v.GetValue(); + return true; + case ValueWord v: + result = v.GetValue(); + return true; + case ValueDWord v: + result = v.GetValue(); + return true; + case ValueLWord v: + result = v.GetValue(); + return true; + case ValueReal v: + result = v.GetValue(); + return true; + case ValueLReal v: + result = v.GetValue(); + return true; + case ValueTimespan v: + result = v.GetValue() / 1_000_000.0; + return true; + default: + result = 0; + return false; + } + } + + private static bool IsFloatingPoint(PValue value) + { + return value is ValueReal or ValueLReal or ValueTimespan; + } + + private static S7CommPlusCpuOperatingState MapCpuOperatingState(int value) + { + return value switch + { + 0 => S7CommPlusCpuOperatingState.NotSupported, + 1 or 3 or 4 or 17 or 33 => S7CommPlusCpuOperatingState.Stop, + 5 or 8 or 9 or 18 or 20 or 37 or 40 => S7CommPlusCpuOperatingState.Run, + 6 or 35 or 38 => S7CommPlusCpuOperatingState.Startup, + 10 or 34 => S7CommPlusCpuOperatingState.Halt, + 13 => S7CommPlusCpuOperatingState.Defective, + _ => S7CommPlusCpuOperatingState.Unknown + }; + } + + private static string MapCpuStateSwitch(int? value) + { + return value switch + { + 1 => "Stop", + 2 => "Run", + 3 => "MRes", + _ => null + }; + } + + + public int GetCpuCultureInfo(out S7CommPlusCpuCultureInfo cultureInfo) + { + int res; + cultureInfo = null; + + var exploreReq = new ExploreRequest(ProtocolVersion.V2); + exploreReq.ExploreId = Ids.NativeObjects_theTextContainer_Rid; + exploreReq.ExploreRequestId = Ids.None; + exploreReq.ExploreChildsRecursive = 0; + exploreReq.ExploreParents = 1; + exploreReq.AddressList.Add(Ids.TextContainer_LCIDs_Aid); + + res = _requests.SendExplore(exploreReq, out var exploreRes); + if (res != 0) + { + return res; + } + + var textContainer = FindObject( + exploreRes.Objects, + obj => obj.RelationId == Ids.NativeObjects_theTextContainer_Rid || obj.ClassId == Ids.TextContainer_Class_Rid); + if (textContainer == null || + !textContainer.Attributes.TryGetValue(Ids.TextContainer_LCIDs_Aid, out var lcidValue)) + { + return S7Consts.errIsoInvalidPDU; + } + + if (lcidValue is ValueUIntArray lcidArray) + { + cultureInfo = new S7CommPlusCpuCultureInfo(lcidArray.GetValue().Select(languageId => (int)languageId)); + return 0; + } + + if (lcidValue is ValueUInt lcid) + { + cultureInfo = new S7CommPlusCpuCultureInfo(new[] { (int)lcid.GetValue() }); + return 0; + } + + return S7Consts.errIsoInvalidPDU; + } + + private static PObject FindObject(IEnumerable objects, Func predicate) + { + if (objects == null) + { + return null; + } + + foreach (var obj in objects) + { + if (predicate(obj)) + { + return obj; + } + + var child = FindObject(obj.GetObjects(), predicate); + if (child != null) + { + return child; + } + } + + return null; + } + + public int GetCpuInfo(out S7CommPlusCpuInfo cpuInfo) + { + cpuInfo = null; + + var res = RunGetVarSubstreamedRequest(Ids.NativeObjects_theASRoot_Rid, 2459, out var pValueVersions); + if (res != 0) + return res; + var arrVersions = ((ValueUSIntArray)pValueVersions).GetValue(); + var version1 = new Version(arrVersions[0], arrVersions[1], arrVersions[2], arrVersions[3]); + var version2 = new Version(arrVersions[4], arrVersions[5], arrVersions[6], arrVersions[7]); + + res = RunGetVarSubstreamedRequest(Ids.NativeObjects_theCPUCommon_Rid, 233, out var pValuePlcName); + if (res != 0) + return res; + var cpuName = ((ValueWString)pValuePlcName).GetValue(); + + res = RunGetVarSubstreamedRequest(Ids.NativeObjects_theCPUProxy_Rid, 3753, out var pValueMlfbSerial); + if (res != 0) + return res; + var data = ((ValueBlob)pValueMlfbSerial).GetValue(); + var mlfb = Encoding.ASCII.GetString(data[8..27]); + //space + var serial = Encoding.ASCII.GetString(data[28..44]); + //nulbyte + var hardware = data[46]; + var firmware = new Version(data[47], data[48], data[49], data[50]); + //data maybe conatins more: + //rack (1byte), slot (1byte), + + res = RunGetVarSubstreamedRequest(Ids.ReleaseMngmtRoot_Rid, 8342, out var pValueVersionsAndName); + if (res != 0) + return res; + var versionsAndName = ((ValueWStringArray)pValueVersionsAndName).GetValue(); + + cpuInfo = new S7CommPlusCpuInfo() + { + PlcName = cpuName, + ProjectName = versionsAndName[2], + VersionTia = version1, + Version2 = version2, + CpuMlfb = mlfb, + CpuSerial = serial, + CpuFirmware = firmware + }; + + return 0; + } + } +} diff --git a/src/S7CommPlusDriver/Services/S7CommPlusTagSubscriptionService.cs b/src/S7CommPlusDriver/Services/S7CommPlusTagSubscriptionService.cs new file mode 100644 index 0000000..00858bf --- /dev/null +++ b/src/S7CommPlusDriver/Services/S7CommPlusTagSubscriptionService.cs @@ -0,0 +1,278 @@ +#region License +/****************************************************************************** + * S7CommPlusDriver + * + * Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de + * + * This file is part of S7CommPlusDriver. + * + * S7CommPlusDriver is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + /****************************************************************************/ +#endregion + +using System; +using System.Collections.Generic; +using S7CommPlusDriver.ClientApi; +using S7CommPlusDriver.Internal; + +namespace S7CommPlusDriver +{ + internal sealed class S7CommPlusTagSubscriptionService + { + private readonly IS7CommPlusProtocolSession _session; + private readonly S7CommPlusProtocolRequests _requests; + + public S7CommPlusTagSubscriptionService(IS7CommPlusProtocolSession session) + { + _session = session; + _requests = new S7CommPlusProtocolRequests(session); + } + + private readonly Dictionary _subscriptions = new Dictionary(); + byte m_SubcriptionChangeCounter = 1; + uint m_SubscriptionRelationId = S7CommPlusProtocolConstants.SubscriptionRelationIdStart; + + private sealed class TagSubscriptionState + { + public Dictionary SubscribedTags { get; } = new Dictionary(); + public short NextCreditLimit { get; set; } + } + + /// + /// Creates a subscription + /// + /// The list of tags to add to the subscription + /// Cycle time for update in milliseconds. Lowest value seems to be 100 ms (if it's not dependant on the CPU). + /// + public int Create(List plcTags, ushort cycleTime) + { + return Create(plcTags, cycleTime, S7CommPlusProtocolConstants.DefaultSubscriptionCreditLimit, out _); + } + + public int Create(List plcTags, ushort cycleTime, short initialCreditLimit, out uint subscriptionObjectId) + { + subscriptionObjectId = 0; + int res; + var state = new TagSubscriptionState(); + var relationId = m_SubscriptionRelationId++; + PObject subsobj = new PObject(); + subsobj.ClassId = Ids.ClassSubscription; + subsobj.RelationId = relationId; + subsobj.AddAttribute(Ids.ObjectVariableTypeName, new ValueWString("Subscription_" + relationId.ToString())); + subsobj.AddAttribute(Ids.SubscriptionFunctionClassId, new ValueUSInt((byte)SubscriptionFunctionClass.Variables)); + subsobj.AddAttribute(Ids.SubscriptionMissedSendings, new ValueUInt(0)); + subsobj.AddAttribute(Ids.SubscriptionSubsystemError, new ValueLInt(0)); + subsobj.AddAttribute(Ids.SubscriptionRouteMode, new ValueUSInt((byte)SubscriptionRouteMode.CyclicAndChangedValues)); + + // Testresults of some RouteModes (0x04, 0x14, 0x20) some applications are using, together with credit limits: + // For Alarm Subscription RouteMode 0x02 is used. + //-----------+-------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------- + // RouteMode | CreditLimit | Behaviour + //-----------+-------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------- + // 0x00 | 0 | No notification at all + // 0x00 | -1 | All values on create; then values that have changed, empty Notification each cycle; unlimited without retriggering; CreditTick always 0 + // 0x00 | n>0 | All values on create; then values that have changed, empty Notification each cycle; stops after CreditTick reaches difference of n when not set to new value + // 0x04 | 0 | Identical to 0x00 / 0 + // 0x04 | -1 | Identical to 0x00 / -1 + // 0x04 | n>0 | Identical to 0x00 / n>0 + // 0x14 | 0 | Identical to 0x00 / 0 + // 0x14 | -1 | Identical to 0x00 / -1 + // 0x14 | n>0 | Identical to 0x00 / n>0 + // 0x20 | 0 | Identical to 0x00 / 0 + // 0x20 | -1 | All values on create; then values that have changed, on cycle without change no notification; unlimited without retriggering; CreditTick always 0 + // 0x20 | n>0 | All values on create; then values that have changed, on cycle without change no notification; stops after CreditTick reaches difference of n when not set to new value + + subsobj.AddAttribute(Ids.SubscriptionActive, new ValueBool(true)); + subsobj.AddAttribute(Ids.SubscriptionReferenceList, GetSubscriptionListArray(plcTags, state.SubscribedTags)); + subsobj.AddAttribute(Ids.SubscriptionCycleTime, new ValueUDInt(cycleTime)); + subsobj.AddAttribute(Ids.SubscriptionDisabled, new ValueUSInt(0)); + subsobj.AddAttribute(Ids.SubscriptionCount, new ValueUSInt(0)); + state.NextCreditLimit = initialCreditLimit; + subsobj.AddAttribute(Ids.SubscriptionCreditLimit, new ValueInt(state.NextCreditLimit)); // -1=unlimited, 255 = max + subsobj.AddAttribute(Ids.SubscriptionTicks, new ValueUInt(S7CommPlusProtocolConstants.SubscriptionTicksUnlimited)); + subsobj.AddAttribute(S7CommPlusProtocolConstants.SubscriptionDefaultAttribute1055, new ValueUSInt(0)); + + // Build the request object + var createObjReq = new CreateObjectRequest(ProtocolVersion.V2, 0, true); + createObjReq.TransportFlags = S7CommPlusProtocolConstants.RequestWithResponseTransportFlags; + createObjReq.RequestId = _session.SessionId2; + createObjReq.RequestValue = new ValueUDInt(0); + createObjReq.SetRequestObject(subsobj); + + // Send it + res = _requests.CreateObject(createObjReq, out var createObjRes); + if (res != 0) + { + _session.DisconnectTransport(); + return res; + } + + if (createObjRes.ReturnValue == 0) + { + subscriptionObjectId = createObjRes.ObjectIds[0]; + _subscriptions[subscriptionObjectId] = state; + } + else + { + // If creating a subscription fails, the object is still created and should be deleted. + // At least deleting it, gives no error. + System.Diagnostics.Trace.WriteLine(String.Format("Subscription - Create: Failed with Returnvalue = 0x{0:X8}", createObjRes.ReturnValue)); + res = S7Consts.errCliInvalidParams; + } + return res; + } + + private int SubscriptionSetCreditLimit(uint subscriptionObjectId, short limit) + { + return _requests.SetSubscriptionCreditLimit(subscriptionObjectId, limit); + } + + private ValueUDIntArray GetSubscriptionListArray(List plcTags, Dictionary subscribedTags) + { + var la = new List(); + // 0x8?ssxxxx = 8 = create/update flag, ss = subscription change counter. + la.Add(S7CommPlusProtocolConstants.SubscriptionListCreateFlag | ((uint)(m_SubcriptionChangeCounter) << 16)); + la.Add(0); // Number of items to unsubscribe + la.Add((uint)plcTags.Count); // Number of items to subscribe + + uint tagReferenceId = 1; + uint head; + foreach (var tag in plcTags) + { + // Save the reference Id in the dictionary. In the notification we get this reference Id back + // and know to which tag the value belongs to. + subscribedTags.Add(tagReferenceId, tag); + // Write the Item address + head = S7CommPlusProtocolConstants.SubscriptionItemAddressHeaderFlag; + // It's not known where 0x8004 stands for -> 4 was a guess it's for the number of fields + // before the LIDs, but that's wrong (coincidentally fits here in this special case). + // Get the number of IDs in advance, Sub-Area counts as one, and then count each LID. + // 0x8aaabbbb = aaa = unknown value, bbbb = number of fields in the 2nd part. + head |= (uint)(1 + tag.Address.LID.Count); + la.Add(head); + la.Add(tagReferenceId); + la.Add(0); // Unknown 1 + la.Add(tag.Address.AccessArea); + la.Add(tag.Address.SymbolCrc); + // Count value in head starts from here + la.Add(tag.Address.AccessSubArea); + foreach(var li in tag.Address.LID) + { + la.Add(li); + } + tagReferenceId++; + } + // Convert all data to protocol UDInt Array (VLQ encoded) + return new ValueUDIntArray(la.ToArray(), S7CommPlusProtocolConstants.ValueAddressArrayFlag); + } + + public int TestWaitForNotifications(int untilNumberOfNotifications) + { + int res = 0; + short creditLimitStep = S7CommPlusProtocolConstants.DefaultSubscriptionCreditLimitStep; + var subscriptionObjectId = 0u; + foreach (var subscription in _subscriptions) + { + subscriptionObjectId = subscription.Key; + break; + } + + if (subscriptionObjectId == 0 || !_subscriptions.TryGetValue(subscriptionObjectId, out var state)) + { + return S7Consts.errCliInvalidParams; + } + + for (int i = 1; i <= untilNumberOfNotifications; i++) + { + System.Diagnostics.Trace.WriteLine(Environment.NewLine + "WaitForNotifications(): *** Loop #" + i.ToString() + " ***"); + var result = _requests.WaitNotification(subscriptionObjectId, 5000, out var noti); + if (result != 0) + { + return result; + } + else + { + System.Diagnostics.Trace.WriteLine("Notification: CreditTick=" + noti.NotificationCreditTick + " SequenceNumber=" + noti.NotificationSequenceNumber); + System.Diagnostics.Trace.WriteLine(String.Format("PLC-Timestamp={0}.{1:D03} ValuesCount={2}", noti.Add1Timestamp.ToString(), noti.Add1Timestamp.Millisecond, noti.Values.Count)); + foreach(var v in noti.Values) + { + System.Diagnostics.Trace.WriteLine("---> key=" + v.Key + " value=" + v.Value.ToString()); + // Notification item errors are one-byte return codes; tag read errors use the 64-bit PLC return value space. + state.SubscribedTags[v.Key].ProcessReadResult(v.Value, 0); + } + + if (noti.NotificationCreditTick >= state.NextCreditLimit - 1) // Set new limit one tick before it expires, to get a constant flow of data + { + // CreditTick in Notification is only one byte + state.NextCreditLimit = (short)((state.NextCreditLimit + creditLimitStep) % 255); + System.Diagnostics.Trace.WriteLine("--> Credit limit of " + noti.NotificationCreditTick + " reached. SetCreditLimit to " + state.NextCreditLimit.ToString()); + SubscriptionSetCreditLimit(subscriptionObjectId, state.NextCreditLimit); + } + } + } + return res; + } + + public int WaitForNotifications(uint subscriptionObjectId, int waitTimeout, short creditLimitStep, out List notifications) + { + notifications = new List(); + if (!_subscriptions.TryGetValue(subscriptionObjectId, out var state)) + { + return S7Consts.errCliInvalidParams; + } + + var result = _requests.WaitNotification(subscriptionObjectId, waitTimeout, out var noti); + if (result != 0) + { + return result; + } + + notifications.Add(noti); + + foreach (var value in noti.Values) + { + if (state.SubscribedTags.TryGetValue(value.Key, out var tag)) + { + tag.ProcessReadResult(value.Value, 0); + } + } + + foreach (var returnValue in noti.ReturnValues) + { + if (state.SubscribedTags.TryGetValue(returnValue.Key, out var tag)) + { + tag.ProcessReadResult(null, returnValue.Value); + } + } + + if (creditLimitStep > 0 && noti.NotificationCreditTick >= state.NextCreditLimit - 1) + { + state.NextCreditLimit = (short)((state.NextCreditLimit + creditLimitStep) % 255); + if (state.NextCreditLimit == 0) + { + state.NextCreditLimit = creditLimitStep; + } + return SubscriptionSetCreditLimit(subscriptionObjectId, state.NextCreditLimit); + } + + return 0; + } + + public int Delete(uint subscriptionObjectId) + { + int res; + if (subscriptionObjectId == 0) + { + return 0; + } + + _subscriptions.Remove(subscriptionObjectId); + System.Diagnostics.Trace.WriteLine(String.Format("SubscriptionDelete: Calling DeleteObject for SubscriptionObjectId={0:X8}", subscriptionObjectId)); + res = _session.DeleteObject(subscriptionObjectId); + return res; + } + } +} diff --git a/src/S7CommPlusDriver/Services/S7CommPlusTextListService.cs b/src/S7CommPlusDriver/Services/S7CommPlusTextListService.cs new file mode 100644 index 0000000..94a74c5 --- /dev/null +++ b/src/S7CommPlusDriver/Services/S7CommPlusTextListService.cs @@ -0,0 +1,218 @@ +using S7CommPlusDriver.Internal; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace S7CommPlusDriver +{ + internal sealed class S7CommPlusTextListService + { + private const uint TextListLibraryBaseRelationId = 0x8a370000; + private readonly S7CommPlusMetadataService _metadata; + private readonly S7CommPlusProtocolRequests _requests; + + public S7CommPlusTextListService(IS7CommPlusProtocolSession session, S7CommPlusMetadataService metadata) + { + _metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); + _requests = new S7CommPlusProtocolRequests(session); + } + + public int GetTextLists(IEnumerable languageIds, out S7CommPlusTextListCatalog catalog) + { + catalog = S7CommPlusTextListCatalog.Empty; + var requestedLanguages = languageIds?.Distinct().ToList(); + if (requestedLanguages == null || requestedLanguages.Count == 0) + { + var cultureResult = _metadata.GetCpuCultureInfo(out var cultureInfo); + if (cultureResult != 0) + { + return cultureResult; + } + + requestedLanguages = cultureInfo.LanguageIds.ToList(); + } + + if (requestedLanguages.Any(languageId => languageId < 0 || languageId > UInt16.MaxValue)) + { + return S7Consts.errCliInvalidParams; + } + + var lists = new List(); + var result = TryReadLibrary(0, TextListLibraryBaseRelationId, S7CommPlusTextListScope.LanguageIndependent, lists); + if (result != 0) + { + return result; + } + + foreach (var languageId in requestedLanguages) + { + if (languageId == 0) + { + continue; + } + + result = TryReadLibrary( + languageId, + TextListLibraryBaseRelationId + (ushort)languageId, + S7CommPlusTextListScope.LanguageSpecific, + lists); + if (result != 0) + { + return result; + } + } + + catalog = new S7CommPlusTextListCatalog(requestedLanguages, lists); + return 0; + } + + private int TryReadLibrary(int languageId, uint relationId, S7CommPlusTextListScope scope, List lists) + { + var result = _requests.Explore(relationId, null, out var response, exploreChildsRecursive: 0); + if (result != 0) + { + return result; + } + + var textLibrary = response?.Objects?.FirstOrDefault(obj => obj.ClassId == Ids.TextLibraryClassRID); + if (textLibrary == null) + { + return 0; + } + + if (!textLibrary.Attributes.TryGetValue(Ids.TextLibraryOffsetArea, out var offsetAreaValue) || + !textLibrary.Attributes.TryGetValue(Ids.TextLibraryStringArea, out var stringAreaValue) || + offsetAreaValue is not ValueBlobArray offsetArea || + stringAreaValue is not ValueBlob stringArea) + { + return S7Consts.errIsoInvalidPDU; + } + + var offsets = offsetArea.GetValue(); + var strings = stringArea.GetValue(); + if (offsets == null || offsets.Length < 2 || strings == null) + { + return S7Consts.errIsoInvalidPDU; + } + + return DecodeTextListLibrary(offsets[0].GetValue(), offsets[1].GetValue(), strings, languageId, scope, lists); + } + + private static int DecodeTextListLibrary( + byte[] listTable, + byte[] entryTable, + byte[] stringTable, + int languageId, + S7CommPlusTextListScope scope, + List lists) + { + if (listTable == null || entryTable == null || stringTable == null || listTable.Length < 20) + { + return S7Consts.errIsoInvalidPDU; + } + + var pos = 16u; + if (!TryReadUInt32(listTable, pos, out var listCount)) + { + return S7Consts.errIsoInvalidPDU; + } + pos += 4; + + for (var i = 0; i < listCount; i++) + { + if (!TryReadUInt16(listTable, pos, out var listId) || + !TryReadUInt32(listTable, pos + 2, out var entryOffset)) + { + return S7Consts.errIsoInvalidPDU; + } + pos += 6; + + if (!TryReadEntries(entryTable, stringTable, entryOffset, out var entries)) + { + return S7Consts.errIsoInvalidPDU; + } + + lists.Add(new S7CommPlusTextList(listId, languageId, scope, ClassifyTextList(listId), entries)); + } + + return 0; + } + + private static S7CommPlusTextListType ClassifyTextList(int listId) + { + // The online text-library payload only carries runtime list ids. + // Keep the public API unified and expose this as a best-effort + // classification for callers that want to display/filter it. + return listId >= 512 && listId < 32768 + ? S7CommPlusTextListType.User + : S7CommPlusTextListType.System; + } + + private static bool TryReadEntries(byte[] entryTable, byte[] stringTable, uint entryOffset, out List entries) + { + entries = null; + if (!TryReadUInt32(entryTable, entryOffset, out var entryCount)) + { + return false; + } + + entries = new List(); + var pos = entryOffset + 4; + for (var i = 0; i < entryCount; i++) + { + if (!TryReadUInt16(entryTable, pos, out var value) || + !TryReadUInt32(entryTable, pos + 2, out var stringOffset) || + !TryReadText(stringTable, stringOffset, out var text)) + { + return false; + } + pos += 6; + entries.Add(new S7CommPlusTextListEntry(value, value, text)); + } + + return true; + } + + private static bool TryReadText(byte[] stringTable, uint offset, out string text) + { + text = null; + if (!TryReadUInt16(stringTable, offset, out var length)) + { + return false; + } + + var start = offset + 2; + if (start > stringTable.Length || start + length > stringTable.Length) + { + return false; + } + + text = Utils.GetUtfString(stringTable, start, length); + return true; + } + + private static bool TryReadUInt16(byte[] data, uint offset, out ushort value) + { + value = 0; + if (data == null || offset + 2 > data.Length) + { + return false; + } + + value = Utils.GetUInt16LE(data, offset); + return true; + } + + private static bool TryReadUInt32(byte[] data, uint offset, out uint value) + { + value = 0; + if (data == null || offset + 4 > data.Length) + { + return false; + } + + value = Utils.GetUInt32LE(data, offset); + return true; + } + } +} diff --git a/src/S7CommPlusDriver/Services/S7CommPlusTisWatchSubscriptionService.cs b/src/S7CommPlusDriver/Services/S7CommPlusTisWatchSubscriptionService.cs new file mode 100644 index 0000000..227dc79 --- /dev/null +++ b/src/S7CommPlusDriver/Services/S7CommPlusTisWatchSubscriptionService.cs @@ -0,0 +1,443 @@ +using S7CommPlusDriver.Internal; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Linq; + +namespace S7CommPlusDriver +{ + internal sealed class S7CommPlusTisWatchSubscriptionService + { + private const uint TisResultReferenceId = 9; + private const uint TisNotificationCreditReferenceId = 10; + private const uint TisEnabledActualReferenceId = 11; + + private readonly IS7CommPlusProtocolSession _session; + private readonly S7CommPlusProtocolRequests _requests; + private readonly Dictionary _subscriptions = new Dictionary(); + + public S7CommPlusTisWatchSubscriptionService(IS7CommPlusProtocolSession session) + { + _session = session; + _requests = new S7CommPlusProtocolRequests(session); + } + + public string LastDiagnostic { get; private set; } = ""; + + private sealed class TisWatchState + { + public S7CommPlusTisResultModel ResultModel { get; set; } = new S7CommPlusTisResultModel(); + public uint JobObjectId { get; set; } + public uint SubscriptionObjectId { get; set; } + public uint SubscriptionRefObjectId { get; set; } + public uint PollSequenceNumber { get; set; } + } + + public int Create(S7CommPlusTisWatchRequest request, out uint subscriptionObjectId) + { + subscriptionObjectId = 0; + if (request == null) + return S7Consts.errCliInvalidParams; + + request.LastLifecycleStage = "create TIS watch job"; + LastDiagnostic = ""; + var state = new TisWatchState + { + ResultModel = request.ResultModel?.Clone() ?? new S7CommPlusTisResultModel() + }; + + var job = new PObject + { + ClassId = Ids.TisWatchJob_Class_Rid, + RelationId = Ids.GetNewRIDOnServer + }; + job.AddAttribute(Ids.ObjectVariableTypeName, new ValueWString(request.JobName)); + job.AddAttribute(Ids.AbstractTisJob_Request, new ValueBlob(0, request.RequestBlob)); + job.AddAttribute(Ids.AbstractTisJob_Trigger, new ValueBlob(0, request.TriggerBlob)); + job.AddAttribute(Ids.AbstractTisJob_ModifyingJob, new ValueBool(false)); + + var createJob = new CreateObjectRequest(ProtocolVersion.V2, 0, true) + { + TransportFlags = S7CommPlusProtocolConstants.RequestWithResponseTransportFlags, + RequestId = _session.SessionId, + RequestValue = new ValueUDInt(0) + }; + createJob.SetRequestObject(job); + + var res = _requests.CreateObject(createJob, out var createJobResponse); + if (res != 0) + { + _session.DisconnectTransport(); + return res; + } + + if (createJobResponse.ReturnValue != 0 || createJobResponse.ObjectIds.Count == 0) + { + return S7Consts.errCliInvalidParams; + } + + state.JobObjectId = createJobResponse.ObjectIds[0]; + + request.LastLifecycleStage = "create TIS watch subscription"; + res = CreateSubscription(request.JobName, state); + if (res != 0) + { + CleanupAfterFailedCreate(state); + return res; + } + + request.LastLifecycleStage = "enable TIS watch job and add notification credit"; + res = _requests.SetMultiVariablesRaw( + 0, + new uint[] + { + 0, state.JobObjectId, 1, Ids.AbstractTisJob_TisJobEnabledConf, + 0, state.SubscriptionRefObjectId, 1, Ids.TisSubscriptionRef_IncrementNotificationCredit + }, + new PValue[] + { + new ValueBool(true), + new ValueUSInt(1) + }); + if (res != 0) + { + CleanupAfterFailedCreate(state); + return res; + } + + request.LastLifecycleStage = "started"; + subscriptionObjectId = state.SubscriptionObjectId; + _subscriptions[subscriptionObjectId] = state; + return 0; + } + + private void CleanupAfterFailedCreate(TisWatchState state) + { + if (state.SubscriptionObjectId != 0) + { + _session.DeleteObject(state.SubscriptionObjectId); + state.SubscriptionObjectId = 0; + state.SubscriptionRefObjectId = 0; + } + + if (state.JobObjectId != 0) + { + _session.DeleteObject(state.JobObjectId); + state.JobObjectId = 0; + } + } + + private int CreateSubscription(string jobName, TisWatchState state) + { + var subscription = new PObject + { + ClassId = Ids.ClassSubscription, + RelationId = S7CommPlusProtocolConstants.SubscriptionRelationIdStart + }; + subscription.AddAttribute(Ids.ObjectVariableTypeName, new ValueWString("Subscription_" + jobName)); + subscription.AddAttribute(Ids.SubscriptionFunctionClassId, new ValueUSInt((byte)SubscriptionFunctionClass.Tis)); + subscription.AddAttribute(Ids.SubscriptionMissedSendings, new ValueUInt(0)); + subscription.AddAttribute(Ids.SubscriptionSubsystemError, new ValueLInt(0)); + subscription.AddAttribute(Ids.SubscriptionRouteMode, new ValueUSInt((byte)SubscriptionRouteMode.Tis)); + subscription.AddAttribute(Ids.SubscriptionActive, new ValueBool(true)); + subscription.AddAttribute(Ids.SubscriptionReferenceList, CreateSubscriptionReferenceList(state)); + subscription.AddAttribute(Ids.SubscriptionCycleTime, new ValueUDInt(0)); + subscription.AddAttribute(Ids.SubscriptionDisabled, new ValueUSInt(0)); + subscription.AddAttribute(Ids.SubscriptionCount, new ValueUSInt(0)); + subscription.AddAttribute(Ids.SubscriptionCreditLimit, new ValueInt(-1)); + subscription.AddAttribute(Ids.SubscriptionTicks, new ValueUInt(S7CommPlusProtocolConstants.SubscriptionTicksUnlimited)); + subscription.AddAttribute(S7CommPlusProtocolConstants.SubscriptionDefaultAttribute1055, new ValueUSInt(0)); + + var subscriptionRef = new PObject + { + ClassId = Ids.TisSubscriptionRef_Class_Rid, + RelationId = Ids.GetNewRIDOnServer + }; + subscriptionRef.AddAttribute(Ids.ObjectVariableTypeName, new ValueWString("TisSubscriptionRef_" + jobName)); + subscriptionRef.AddAttribute(Ids.SubscriptionReferenceMode, new ValueUSInt(1)); + subscriptionRef.AddAttribute(Ids.TisSubscriptionRef_IncrementNotificationCredit, new ValueUSInt(0)); + subscriptionRef.AddRelation(Ids.TisSubscriptionRef_itsAssumingJob, state.JobObjectId); + subscription.AddObject(subscriptionRef); + + var createSubscription = new CreateObjectRequest(ProtocolVersion.V2, 0, true) + { + TransportFlags = S7CommPlusProtocolConstants.RequestWithResponseTransportFlags, + RequestId = _session.SessionId2, + RequestValue = new ValueUDInt(0) + }; + createSubscription.SetRequestObject(subscription); + + var res = _requests.CreateObject(createSubscription, out var response); + if (res != 0) + { + _session.DisconnectTransport(); + return res; + } + + if (response.ReturnValue != 0 || response.ObjectIds.Count == 0) + { + return S7Consts.errCliInvalidParams; + } + + state.SubscriptionObjectId = response.ObjectIds[0]; + state.SubscriptionRefObjectId = response.ObjectIds.Count > 1 ? response.ObjectIds[1] : 0; + return state.SubscriptionRefObjectId == 0 ? S7Consts.errCliInvalidParams : 0; + } + + private ValueUDIntArray CreateSubscriptionReferenceList(TisWatchState state) + { + var values = new[] + { + 0x80010000u, 0u, 3u, + 0x80120001u, TisResultReferenceId, _session.SessionId, state.JobObjectId, 0u, (uint)Ids.AbstractTisJob_Result, + 0x80120001u, TisNotificationCreditReferenceId, _session.SessionId, state.JobObjectId, 0u, (uint)Ids.AbstractTisJob_NotificationCredit, + 0x80120001u, TisEnabledActualReferenceId, _session.SessionId, state.JobObjectId, 0u, (uint)Ids.AbstractTisJob_TisJobEnabledActual, + }; + return new ValueUDIntArray(values, S7CommPlusProtocolConstants.ValueAddressArrayFlag); + } + + public int WaitForNotifications(uint subscriptionObjectId, int waitTimeout, out List notifications) + { + notifications = new List(); + if (!_subscriptions.TryGetValue(subscriptionObjectId, out var state)) + { + return S7Consts.errCliInvalidParams; + } + + LastDiagnostic = $"waiting for TIS notification timeout={waitTimeout}ms"; + + var result = _requests.WaitNotification(subscriptionObjectId, waitTimeout, out var notification); + LastDiagnostic = $"WaitNotification returned {result}"; + if (result != 0) + { + if (TryPollResultBuffer(state, out var polledNotification)) + { + notifications.Add(polledNotification); + if (state.SubscriptionRefObjectId != 0) + { + _requests.SetVariable(state.SubscriptionRefObjectId, Ids.TisSubscriptionRef_IncrementNotificationCredit, new ValueUSInt(1)); + } + return 0; + } + + return result; + } + + notifications.Add(ConvertNotification(notification, state)); + return _requests.SetVariable(state.SubscriptionRefObjectId, Ids.TisSubscriptionRef_IncrementNotificationCredit, new ValueUSInt(1)); + } + + private bool TryPollResultBuffer(TisWatchState state, out S7CommPlusTisWatchNotification notification) + { + notification = null; + if (state.JobObjectId == 0) + { + LastDiagnostic = "poll skipped: no TIS job RID"; + return false; + } + + LastDiagnostic = $"polling result buffer with GetVariable(job={state.JobObjectId}, aid={Ids.AbstractTisJob_Result})"; + var result = _requests.GetVariable(state.JobObjectId, (uint)Ids.AbstractTisJob_Result, out var value); + if (result != 0) + { + LastDiagnostic = $"GetVariable(job={state.JobObjectId}, aid={Ids.AbstractTisJob_Result}) failed with {result}"; + result = _requests.GetVarSubstreamed(state.JobObjectId, Ids.AbstractTisJob_Result, out value); + } + + if (result != 0) + { + LastDiagnostic += $"; GetVarSubstreamed fallback failed with {result}"; + return false; + } + + var rawResult = ExtractBlob(value); + if (rawResult.Length == 0) + { + LastDiagnostic = $"poll returned {value?.GetType().Name ?? ""} with empty result blob"; + return false; + } + + LastDiagnostic = $"poll returned {rawResult.Length} result bytes"; + notification = new S7CommPlusTisWatchNotification( + DateTime.UtcNow, + ++state.PollSequenceNumber, + 0, + null, + null, + rawResult, + S7CommPlusTisResultParser.Parse(rawResult, state.ResultModel)); + return true; + } + + private S7CommPlusTisWatchNotification ConvertNotification(Notification notification, TisWatchState state) + { + notification.Values.TryGetValue(TisEnabledActualReferenceId, out var enabledValue); + notification.Values.TryGetValue(TisNotificationCreditReferenceId, out var creditValue); + notification.Values.TryGetValue(TisResultReferenceId, out var resultValue); + + var rawResult = ExtractBlob(resultValue); + var watchPoints = S7CommPlusTisResultParser.Parse(rawResult, state.ResultModel); + return new S7CommPlusTisWatchNotification( + notification.Add1Timestamp, + notification.NotificationSequenceNumber, + notification.NotificationCreditTick, + ExtractBool(enabledValue), + ExtractByte(creditValue), + rawResult, + watchPoints); + } + + public int Delete(uint subscriptionObjectId) + { + var result = 0; + if (!_subscriptions.TryGetValue(subscriptionObjectId, out var state)) + { + return 0; + } + + _subscriptions.Remove(subscriptionObjectId); + if (state.SubscriptionObjectId != 0) + { + _requests.SetVariable(state.SubscriptionObjectId, Ids.SubscriptionDisabled, new ValueUSInt(1)); + var deleteSubscriptionResult = _session.DeleteObject(state.SubscriptionObjectId); + if (result == 0) + result = deleteSubscriptionResult; + state.SubscriptionObjectId = 0; + state.SubscriptionRefObjectId = 0; + } + + if (state.JobObjectId != 0) + { + var deleteJobResult = _session.DeleteObject(state.JobObjectId); + if (result == 0) + result = deleteJobResult; + state.JobObjectId = 0; + } + + return result; + } + + private static bool? ExtractBool(PValue value) + { + return value is ValueBool boolValue ? boolValue.GetValue() : null; + } + + private static byte? ExtractByte(PValue value) + { + return value is ValueUSInt byteValue ? byteValue.GetValue() : null; + } + + private static byte[] ExtractBlob(PValue value) + { + if (value is ValueBlob blob) + return blob.GetValue() ?? Array.Empty(); + + if (value is ValueBlobSparseArray sparseArray) + { + var sparse = sparseArray.GetValue(); + foreach (var key in sparse.Keys.OrderBy(x => x)) + { + var data = sparse[key].value; + if (data != null && data.Length > 0) + return data; + } + } + + return Array.Empty(); + } + } + + internal static class S7CommPlusTisResultParser + { + public static IReadOnlyList Parse(byte[] result, S7CommPlusTisResultModel model) + { + var output = new List(); + if (result == null || model == null) + return output; + + foreach (var watchPoint in model.WatchPoints) + { + var rawWord = TryReadUInt32(result, watchPoint.RloOffset, out var word) ? word : 0; + var executed = rawWord != 0; + var rlo = executed ? (bool?)((rawWord & 0x80000000u) != 0) : null; + var executionCount = rawWord & 0x3fffffffu; + var values = watchPoint.Values + .Select(value => ParseValue(result, value)) + .ToList(); + + output.Add(new S7CommPlusTisWatchPointResult( + watchPoint.NetworkId, + watchPoint.Uid, + watchPoint.Pin, + rlo, + rawWord, + executionCount, + values)); + } + + return output; + } + + private static S7CommPlusTisValueResult ParseValue(byte[] result, S7CommPlusTisValueModel model) + { + var length = Math.Max(0, model.ByteLength); + var raw = new byte[length]; + if (model.ValueOffset >= 0 && model.ValueOffset + length <= result.Length) + Array.Copy(result, model.ValueOffset, raw, 0, length); + + byte? validity = model.ValidityOffset >= 0 && model.ValidityOffset < result.Length + ? result[model.ValidityOffset] + : null; + ushort? validCount = TryReadUInt16(result, model.ValidCountOffset, out var count) ? count : null; + bool? boolValue = IsBoolDataType(model.DataType) || (String.IsNullOrWhiteSpace(model.DataType) && length == 1) + ? raw.Any(item => item != 0) + : null; + if (boolValue.HasValue && model.InvertBool) + boolValue = !boolValue.Value; + + return new S7CommPlusTisValueResult( + model.NetworkId, + model.Uid, + model.Pin, + model.DataType, + raw, + boolValue, + validity, + validCount); + } + + private static bool IsBoolDataType(string type) + { + if (String.IsNullOrWhiteSpace(type)) + return false; + + var decoded = type.Replace(""", "\"", StringComparison.OrdinalIgnoreCase); + return decoded.IndexOf("Bool", StringComparison.OrdinalIgnoreCase) >= 0 || + decoded.IndexOf("Boolean", StringComparison.OrdinalIgnoreCase) >= 0; + } + + private static bool TryReadUInt32(byte[] data, int offset, out uint value) + { + if (offset >= 0 && offset + 4 <= data.Length) + { + value = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(offset, 4)); + return true; + } + + value = 0; + return false; + } + + private static bool TryReadUInt16(byte[] data, int offset, out ushort value) + { + if (offset >= 0 && offset + 2 <= data.Length) + { + value = BinaryPrimitives.ReadUInt16BigEndian(data.AsSpan(offset, 2)); + return true; + } + + value = 0; + return false; + } + } +} diff --git a/src/S7CommPlusDriver/Subscriptions/Subscription.cs b/src/S7CommPlusDriver/Subscriptions/Subscription.cs deleted file mode 100644 index a489edf..0000000 --- a/src/S7CommPlusDriver/Subscriptions/Subscription.cs +++ /dev/null @@ -1,234 +0,0 @@ -#region License -/****************************************************************************** - * S7CommPlusDriver - * - * Copyright (C) 2023 Thomas Wiens, th.wiens@gmx.de - * - * This file is part of S7CommPlusDriver. - * - * S7CommPlusDriver is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - /****************************************************************************/ -#endregion - -using System; -using System.Collections.Generic; -using S7CommPlusDriver.ClientApi; - -namespace S7CommPlusDriver -{ - partial class S7CommPlusConnection - { - // ************************************************* - // * IMPORTANT! * - // * This is basically a test for subscriptions, * - // * how to use them, and how to later integrate * - // * this into the complete library! * - // ************************************************* - - Dictionary m_SubscribedTags; // ItemRefId - byte m_SubcriptionChangeCounter = 1; - uint m_SubscriptionRelationId = 0x7fffc001; // TODO! Unknown value!0x7fffc001. Seems to be a startvalue, increases on next CreateObject. Guess: It's stored in the plc under this id. - short m_NextCreditLimit; - uint m_SubscriptionObjectId; - - /// - /// Creates a subscription - /// - /// The list of tags to add to the subscription - /// Cycle time for update in milliseconds. Lowest value seems to be 100 ms (if it's not dependant on the CPU). - /// - public int SubscriptionCreate(List plcTags, ushort cycleTime) - { - int res; - m_SubscribedTags = new Dictionary(); - PObject subsobj = new PObject(); - subsobj.ClassId = Ids.ClassSubscription; - subsobj.RelationId = m_SubscriptionRelationId; - subsobj.AddAttribute(Ids.ObjectVariableTypeName, new ValueWString("Subscription_" + m_SubscriptionRelationId.ToString())); - subsobj.AddAttribute(Ids.SubscriptionFunctionClassId, new ValueUSInt(0)); - subsobj.AddAttribute(Ids.SubscriptionMissedSendings, new ValueUInt(0)); - subsobj.AddAttribute(Ids.SubscriptionSubsystemError, new ValueLInt(0)); - subsobj.AddAttribute(Ids.SubscriptionRouteMode, new ValueUSInt(0x14)); // TODO Unknown, mostly seen 0x04, 0x14 or 0x15. Needs to be tested - - // Testresults of some RouteModes (0x04, 0x14, 0x20) some applications are using, together with credit limits: - // For Alarm Subscription RouteMode 0x02 is used. - //-----------+-------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------- - // RouteMode | CreditLimit | Behaviour - //-----------+-------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------- - // 0x00 | 0 | No notification at all - // 0x00 | -1 | All values on create; then values that have changed, empty Notification each cycle; unlimited without retriggering; CreditTick always 0 - // 0x00 | n>0 | All values on create; then values that have changed, empty Notification each cycle; stops after CreditTick reaches difference of n when not set to new value - // 0x04 | 0 | Identical to 0x00 / 0 - // 0x04 | -1 | Identical to 0x00 / -1 - // 0x04 | n>0 | Identical to 0x00 / n>0 - // 0x14 | 0 | Identical to 0x00 / 0 - // 0x14 | -1 | Identical to 0x00 / -1 - // 0x14 | n>0 | Identical to 0x00 / n>0 - // 0x20 | 0 | Identical to 0x00 / 0 - // 0x20 | -1 | All values on create; then values that have changed, on cycle without change no notification; unlimited without retriggering; CreditTick always 0 - // 0x20 | n>0 | All values on create; then values that have changed, on cycle without change no notification; stops after CreditTick reaches difference of n when not set to new value - - subsobj.AddAttribute(Ids.SubscriptionActive, new ValueBool(true)); - subsobj.AddAttribute(Ids.SubscriptionReferenceList, GetSubscriptionListArray(plcTags)); - subsobj.AddAttribute(Ids.SubscriptionCycleTime, new ValueUDInt(cycleTime)); - subsobj.AddAttribute(Ids.SubscriptionDisabled, new ValueUSInt(0)); - subsobj.AddAttribute(Ids.SubscriptionCount, new ValueUSInt(0)); - m_NextCreditLimit = 10; - subsobj.AddAttribute(Ids.SubscriptionCreditLimit, new ValueInt(m_NextCreditLimit)); // -1=unlimited, 255 = max - subsobj.AddAttribute(Ids.SubscriptionTicks, new ValueUInt(65535)); - // 1055 = Unknown -> is working without setting this. - subsobj.AddAttribute(1055, new ValueUSInt(0)); - - // Build the request object - var createObjReq = new CreateObjectRequest(ProtocolVersion.V2, 0, true); - createObjReq.TransportFlags = 0x34; - createObjReq.RequestId = SessionId2; - createObjReq.RequestValue = new ValueUDInt(0); - createObjReq.SetRequestObject(subsobj); - - // Send it - res = SendS7plusFunctionObject(createObjReq); - if (res != 0) - { - m_client.Disconnect(); - return res; - } - m_LastError = 0; - WaitForNewS7plusReceived(m_ReadTimeout); - if (m_LastError != 0) - { - m_client.Disconnect(); - return m_LastError; - } - - var createObjRes = CreateObjectResponse.DeserializeFromPdu(m_ReceivedPDU); - if (createObjRes == null) - { - Console.WriteLine("Subscription - Create: CreateObjectResponse with Error!"); - return S7Consts.errIsoInvalidPDU; - } - - if (createObjRes.ReturnValue == 0) - { - // Save the ObjectId, to modify the existing subscription if needed - m_SubscriptionObjectId = createObjRes.ObjectIds[0]; - } - else - { - // If creating a subscription fails, the object is still created and should be deleted. - // At least deleting it, gives no error. - Console.WriteLine(String.Format("Subscription - Create: Failed with Returnvalue = 0x{0:X8}", createObjRes.ReturnValue)); - res = S7Consts.errCliInvalidParams; - } - return res; - } - - private int SubscriptionSetCreditLimit(short limit) - { - int res; - var setVarReq = new SetVariableRequest(ProtocolVersion.V2); - setVarReq.TransportFlags = 0x74; // Set flag, that we need no response - setVarReq.InObjectId = m_SubscriptionObjectId; - setVarReq.Address = Ids.SubscriptionCreditLimit; - setVarReq.Value = new ValueInt(limit); - res = SendS7plusFunctionObject(setVarReq); - return res; - } - - private ValueUDIntArray GetSubscriptionListArray(List plcTags) - { - var la = new List(); - // 0x8?ssxxxx = 8 = flag CreateNew, ss = 1 byte subscription Change counter, xxxx = unknown - la.Add(0x80000000 | ((uint)(m_SubcriptionChangeCounter) << 16)); - la.Add(0); // Number of items to unsubscribe - la.Add((uint)plcTags.Count); // Number of items to subscribe - - uint tagReferenceId = 1; - uint head; - foreach (var tag in plcTags) - { - // Save the reference Id in the dictionary. In the notification we get this reference Id back - // and know to which tag the value belongs to. - m_SubscribedTags.Add(tagReferenceId, tag); - // Write the Item address - head = 0x80040000; - // It's not known where 0x8004 stands for -> 4 was a guess it's for the number of fields - // before the LIDs, but that's wrong (coincidentally fits here in this special case). - // Get the number of IDs in advance, Sub-Area counts as one, and then count each LID. - // 0x8aaabbbb = aaa = unknown value, bbbb = number of fields in the 2nd part. - head |= (uint)(1 + tag.Address.LID.Count); - la.Add(head); - la.Add(tagReferenceId); - la.Add(0); // Unknown 1 - la.Add(tag.Address.AccessArea); - la.Add(tag.Address.SymbolCrc); - // Count value in head starts from here - la.Add(tag.Address.AccessSubArea); - foreach(var li in tag.Address.LID) - { - la.Add(li); - } - tagReferenceId++; - } - // Convert all data to protocol UDInt Array (VLQ encoded) - return new ValueUDIntArray(la.ToArray(), 0x20); // 0x20 -> Adressarray - } - - public int TestWaitForVariableChangeNotifications(int untilNumberOfNotifications) - { - int res = 0; - short creditLimitStep = 5; - - for (int i = 1; i <= untilNumberOfNotifications; i++) - { - Console.WriteLine(Environment.NewLine + "WaitForNotifications(): *** Loop #" + i.ToString() + " ***"); - m_LastError = 0; - WaitForNewS7plusReceived(5000); - if (m_LastError != 0) - { - return m_LastError; - } - - var noti = Notification.DeserializeFromPdu(m_ReceivedPDU); - if (noti == null) - { - Console.WriteLine("Notification == null!"); - return S7Consts.errIsoInvalidPDU; - } - else - { - Console.Write("Notification: CreditTick=" + noti.NotificationCreditTick + " SequenceNumber=" + noti.NotificationSequenceNumber); - Console.WriteLine(String.Format(" PLC-Timestamp={0}.{1:D03} ValuesCount={2}", noti.Add1Timestamp.ToString(), noti.Add1Timestamp.Millisecond, noti.Values.Count)); - foreach(var v in noti.Values) - { - Console.WriteLine("---> key=" + v.Key + " value=" + v.Value.ToString()); - // Error value in tags expects a 64 bit value, in subscriptions it's only 1 byte (for it's not known what all values are for -> TODO) - m_SubscribedTags[v.Key].ProcessReadResult(v.Value, 0); - } - - if (noti.NotificationCreditTick >= m_NextCreditLimit - 1) // Set new limit one tick before it expires, to get a constant flow of data - { - // CreditTick in Notification is only one byte - m_NextCreditLimit = (short)((m_NextCreditLimit + creditLimitStep) % 255); - Console.WriteLine("--> Credit limit of " + noti.NotificationCreditTick + " reached. SetCreditLimit to " + m_NextCreditLimit.ToString()); - SubscriptionSetCreditLimit(m_NextCreditLimit); - } - } - } - return res; - } - - public int SubscriptionDelete() - { - int res; - m_SubscribedTags.Clear(); - m_SubscriptionObjectId = 0; - Console.WriteLine(String.Format("SubscriptionDelete: Calling DeleteObject for SessionId2={0:X8}", SessionId2)); - res = DeleteObject(SessionId2); - return res; - } - } -} diff --git a/src/S7CommPlusDriver/Tls/BouncyCastleTlsConnector.cs b/src/S7CommPlusDriver/Tls/BouncyCastleTlsConnector.cs new file mode 100644 index 0000000..3aa1d2a --- /dev/null +++ b/src/S7CommPlusDriver/Tls/BouncyCastleTlsConnector.cs @@ -0,0 +1,263 @@ +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Tls; +using Org.BouncyCastle.Tls.Crypto.Impl.BC; +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Threading; +using TlsProtocolVersion = Org.BouncyCastle.Tls.ProtocolVersion; + +namespace S7CommPlusDriver.Tls +{ + internal sealed class BouncyCastleTlsConnector : IS7TlsConnector + { + private const string OmsExporterLabel = "EXPERIMENTAL_OMS"; + private const int OmsExporterSecretLength = 32; + + private readonly IS7TlsConnectorCallback _dataSink; + private readonly BlockingTlsRecordStream _recordStream; + private readonly TlsClientProtocol _protocol; + private readonly PlcTlsClient _tlsClient; + private readonly BlockingCollection _decryptedData = new BlockingCollection(); + private readonly Thread _readerThread; + private bool _disposed; + + public BouncyCastleTlsConnector(IS7TlsConnectorCallback dataSink) + { + _dataSink = dataSink ?? throw new ArgumentNullException(nameof(dataSink)); + _recordStream = new BlockingTlsRecordStream(_dataSink); + _protocol = new TlsClientProtocol(_recordStream); + _tlsClient = new PlcTlsClient(); + + _readerThread = new Thread(ReadDecryptedData) + { + IsBackground = true, + Name = "S7CommPlus BouncyCastle TLS reader" + }; + } + + public void StartHandshake() + { + ThrowIfDisposed(); + _protocol.Connect(_tlsClient); + _readerThread.Start(); + } + + public void Write(byte[] data, int dataLength) + { + ThrowIfDisposed(); + _protocol.Stream.Write(data, 0, dataLength); + _protocol.Stream.Flush(); + } + + public void ReadCompleted(byte[] data, int dataLength) + { + if (_disposed) + { + return; + } + + _recordStream.AddIncoming(data, dataLength); + } + + public int Receive(ref byte[] buffer, int bufferSize) + { + ThrowIfDisposed(); + + if (!_decryptedData.TryTake(out var data)) + { + return 0; + } + + var bytesToCopy = Math.Min(bufferSize, data.Length); + Buffer.BlockCopy(data, 0, buffer, 0, bytesToCopy); + return bytesToCopy; + } + + public byte[] GetOmsExporterSecret() + { + ThrowIfDisposed(); + return _tlsClient.Context.ExportKeyingMaterial(OmsExporterLabel, null, OmsExporterSecretLength); + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _recordStream.Complete(); + _decryptedData.CompleteAdding(); + _protocol.Close(); + } + + private void ReadDecryptedData() + { + var buffer = new byte[8192]; + try + { + while (!_disposed) + { + var bytesRead = _protocol.Stream.Read(buffer, 0, buffer.Length); + if (bytesRead <= 0) + { + break; + } + + var data = new byte[bytesRead]; + Buffer.BlockCopy(buffer, 0, data, 0, bytesRead); + _decryptedData.Add(data); + _dataSink.OnDataAvailable(); + } + } + catch (Exception ex) + { + if (!_disposed) + { + _dataSink.OnSslError(-1, ex.Message); + } + } + } + + private void ThrowIfDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(BouncyCastleTlsConnector)); + } + } + + private sealed class PlcTlsClient : DefaultTlsClient + { + public PlcTlsClient() + : base(new BcTlsCrypto(new SecureRandom())) + { + } + + public TlsClientContext Context { get; private set; } + + public override void Init(TlsClientContext context) + { + Context = context; + base.Init(context); + } + + public override int[] GetCipherSuites() + { + return new[] + { + CipherSuite.TLS_AES_256_GCM_SHA384, + CipherSuite.TLS_AES_128_GCM_SHA256 + }; + } + + protected override TlsProtocolVersion[] GetSupportedVersions() + { + return TlsProtocolVersion.TLSv13.Only(); + } + + public override TlsAuthentication GetAuthentication() + { + return new AcceptAnyServerCertificateAuthentication(); + } + } + + private sealed class AcceptAnyServerCertificateAuthentication : TlsAuthentication + { + public void NotifyServerCertificate(TlsServerCertificate serverCertificate) + { + } + + public TlsCredentials GetClientCredentials(CertificateRequest certificateRequest) + { + return null; + } + } + + private sealed class BlockingTlsRecordStream : Stream + { + private readonly IS7TlsConnectorCallback _dataSink; + private readonly BlockingCollection _incoming = new BlockingCollection(); + private byte[] _currentReadBuffer; + private int _currentReadOffset; + private bool _completed; + + public BlockingTlsRecordStream(IS7TlsConnectorCallback dataSink) + { + _dataSink = dataSink; + } + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length => throw new NotSupportedException(); + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public void AddIncoming(byte[] data, int dataLength) + { + if (_completed) + { + return; + } + + var copy = new byte[dataLength]; + Buffer.BlockCopy(data, 0, copy, 0, dataLength); + _incoming.Add(copy); + } + + public void Complete() + { + _completed = true; + _incoming.CompleteAdding(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + while (_currentReadBuffer == null || _currentReadOffset >= _currentReadBuffer.Length) + { + try + { + _currentReadBuffer = _incoming.Take(); + _currentReadOffset = 0; + } + catch (InvalidOperationException) + { + return 0; + } + } + + var bytesToCopy = Math.Min(count, _currentReadBuffer.Length - _currentReadOffset); + Buffer.BlockCopy(_currentReadBuffer, _currentReadOffset, buffer, offset, bytesToCopy); + _currentReadOffset += bytesToCopy; + return bytesToCopy; + } + + public override void Write(byte[] buffer, int offset, int count) + { + var data = new byte[count]; + Buffer.BlockCopy(buffer, offset, data, 0, count); + _dataSink.WriteData(data, data.Length); + } + + public override void Flush() + { + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + } + } +} diff --git a/src/S7CommPlusDriver/build/net8.0/CopyNativeFiles.targets b/src/S7CommPlusDriver/build/net8.0/CopyNativeFiles.targets new file mode 100644 index 0000000..a7f0ec2 --- /dev/null +++ b/src/S7CommPlusDriver/build/net8.0/CopyNativeFiles.targets @@ -0,0 +1,23 @@ + + + + osx-arm64 + + + win-arm64 + + + win-x64 + + + win-x86 + + + + + + + diff --git a/src/S7CommPlusDriver/runtimes/info.txt b/src/S7CommPlusDriver/runtimes/info.txt new file mode 100644 index 0000000..ba13b34 --- /dev/null +++ b/src/S7CommPlusDriver/runtimes/info.txt @@ -0,0 +1,17 @@ +Runtime Libs: + +MacOS: + + Arm64: 3.4.0 + ./Configure darwin64-arm64-cc --prefix="/tmp/openssl-arm" no-asm + make + make install + + X64: 3.4.0 + ./Configure darwin64-x64-cc --prefix="/tmp/openssl-x64" + make + make install + +Windows + + all versions from: https://slproweb.com/products/Win32OpenSSL.html \ No newline at end of file diff --git a/src/S7CommPlusDriver/runtimes/osx-arm64/native/libcrypto.3.dylib b/src/S7CommPlusDriver/runtimes/osx-arm64/native/libcrypto.3.dylib new file mode 100644 index 0000000..4cc9236 Binary files /dev/null and b/src/S7CommPlusDriver/runtimes/osx-arm64/native/libcrypto.3.dylib differ diff --git a/src/S7CommPlusDriver/runtimes/osx-arm64/native/libssl.3.dylib b/src/S7CommPlusDriver/runtimes/osx-arm64/native/libssl.3.dylib new file mode 100644 index 0000000..f500d94 Binary files /dev/null and b/src/S7CommPlusDriver/runtimes/osx-arm64/native/libssl.3.dylib differ diff --git a/src/S7CommPlusDriver/runtimes/win-arm64/native/libcrypto-3-arm64.dll b/src/S7CommPlusDriver/runtimes/win-arm64/native/libcrypto-3-arm64.dll new file mode 100644 index 0000000..f0196a5 Binary files /dev/null and b/src/S7CommPlusDriver/runtimes/win-arm64/native/libcrypto-3-arm64.dll differ diff --git a/src/S7CommPlusDriver/runtimes/win-arm64/native/libssl-3-arm64.dll b/src/S7CommPlusDriver/runtimes/win-arm64/native/libssl-3-arm64.dll new file mode 100644 index 0000000..5143603 Binary files /dev/null and b/src/S7CommPlusDriver/runtimes/win-arm64/native/libssl-3-arm64.dll differ diff --git a/src/S7CommPlusDriver/runtimes/win-arm64/native/vcruntime140.dll b/src/S7CommPlusDriver/runtimes/win-arm64/native/vcruntime140.dll new file mode 100644 index 0000000..25182bb Binary files /dev/null and b/src/S7CommPlusDriver/runtimes/win-arm64/native/vcruntime140.dll differ diff --git a/src/S7CommPlusDriver/runtimes/win-x64/native/libcrypto-3-x64.dll b/src/S7CommPlusDriver/runtimes/win-x64/native/libcrypto-3-x64.dll new file mode 100644 index 0000000..1f50b91 Binary files /dev/null and b/src/S7CommPlusDriver/runtimes/win-x64/native/libcrypto-3-x64.dll differ diff --git a/src/S7CommPlusDriver/runtimes/win-x64/native/libssl-3-x64.dll b/src/S7CommPlusDriver/runtimes/win-x64/native/libssl-3-x64.dll new file mode 100644 index 0000000..c68aa73 Binary files /dev/null and b/src/S7CommPlusDriver/runtimes/win-x64/native/libssl-3-x64.dll differ diff --git a/src/S7CommPlusDriver/runtimes/win-x86/native/libcrypto-3.dll b/src/S7CommPlusDriver/runtimes/win-x86/native/libcrypto-3.dll new file mode 100644 index 0000000..cdd5684 Binary files /dev/null and b/src/S7CommPlusDriver/runtimes/win-x86/native/libcrypto-3.dll differ diff --git a/src/S7CommPlusDriver/runtimes/win-x86/native/libssl-3.dll b/src/S7CommPlusDriver/runtimes/win-x86/native/libssl-3.dll new file mode 100644 index 0000000..dd52338 Binary files /dev/null and b/src/S7CommPlusDriver/runtimes/win-x86/native/libssl-3.dll differ diff --git a/src/S7CommPlusGUIBrowser/App.config b/src/S7CommPlusGUIBrowser/App.config deleted file mode 100644 index 56efbc7..0000000 --- a/src/S7CommPlusGUIBrowser/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/S7CommPlusGUIBrowser/Form1.cs b/src/S7CommPlusGUIBrowser/Form1.cs index 42e5539..9480c31 100644 --- a/src/S7CommPlusGUIBrowser/Form1.cs +++ b/src/S7CommPlusGUIBrowser/Form1.cs @@ -1,34 +1,28 @@ -using S7CommPlusDriver; +using S7CommPlusDriver; using S7CommPlusDriver.ClientApi; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; +using System.Threading.Tasks; using System.Windows.Forms; namespace S7CommPlusGUIBrowser { public partial class Form1 : Form { - - private S7CommPlusConnection conn = null; + private S7CommPlusClient client; public Form1() { InitializeComponent(); string[] args = Environment.GetCommandLineArgs(); - // 1st argument can be the plc ip-address, otherwise use default if (args.Length >= 2) { tbIpAddress.Text = args[1]; } - // 2nd argument can be the plc password, otherwise use default (no password) if (args.Length >= 3) { tbPassword.Text = args[2]; } - // 3rd argument can be the plc username, otherwise use default (no username) if (args.Length >= 4) { tbUser.Text = args[3]; @@ -41,366 +35,166 @@ private void setStatus(string status) lbStatus.Refresh(); } - private void btnConnect_Click(object sender, EventArgs e) + private async void btnConnect_Click(object sender, EventArgs e) { setStatus("connecting..."); + await CloseClientAsync().ConfigureAwait(true); - if (conn != null) conn.Disconnect(); - conn = new S7CommPlusConnection(); - int res = conn.Connect(tbIpAddress.Text, tbPassword.Text, tbUser.Text); - if (res != 0) + client = new S7CommPlusClient(new S7CommPlusClientOptions { - setStatus("error: " + S7Client.ErrorText(res)); - return; - } - setStatus("connected"); + Address = tbIpAddress.Text, + Password = tbPassword.Text, + Username = tbUser.Text + }); - treeView1.Nodes.Clear(); - setStatus("loading..."); - List dbInfoList; - res = conn.GetListOfDatablocks(out dbInfoList); - if (res != 0) + try { - setStatus("error: " + S7Client.ErrorText(res)); - return; + await client.ConnectAsync().ConfigureAwait(true); + setStatus("loading..."); + await LoadBrowseTreeAsync().ConfigureAwait(true); + setStatus("connected"); } - TreeNode tn; - foreach (S7CommPlusConnection.DatablockInfo dbInfo in dbInfoList) + catch (S7CommPlusException ex) { - tn = treeView1.Nodes.Add(dbInfo.db_name); - tn.Nodes.Add("Loading..."); - tn.Tag = dbInfo.db_block_ti_relid; - tn.ImageKey = "Datablock"; - tn.SelectedImageKey = tn.ImageKey; + setStatus("error: " + ex.Message); + await CloseClientAsync().ConfigureAwait(true); } - // Inputs - tn = treeView1.Nodes.Add("Inputs"); - tn.Nodes.Add("Loading..."); - tn.Tag = 0x90010000; - tn.ImageKey = "Default"; - tn.SelectedImageKey = tn.ImageKey; - // Outputs - tn = treeView1.Nodes.Add("Outputs"); - tn.Nodes.Add("Loading..."); - tn.Tag = 0x90020000; - tn.ImageKey = "Default"; - tn.SelectedImageKey = tn.ImageKey; - // Merker - tn = treeView1.Nodes.Add("Merker"); - tn.Nodes.Add("Loading..."); - tn.Tag = 0x90030000; - tn.ImageKey = "Default"; - tn.SelectedImageKey = tn.ImageKey; - // S7Timers - tn = treeView1.Nodes.Add("S7Timers"); - tn.Nodes.Add("Loading..."); - tn.Tag = 0x90050000; - tn.ImageKey = "Default"; - tn.SelectedImageKey = tn.ImageKey; - // S7Counters - tn = treeView1.Nodes.Add("S7Counters"); - tn.Nodes.Add("Loading..."); - tn.Tag = 0x90060000; - tn.ImageKey = "Default"; - tn.SelectedImageKey = tn.ImageKey; - - setStatus("connected"); } - private void btnDisconnect_Click(object sender, EventArgs e) + private async void btnDisconnect_Click(object sender, EventArgs e) { setStatus("disconnecting..."); - - if (conn != null) conn.Disconnect(); - conn = null; + await CloseClientAsync().ConfigureAwait(true); treeView1.Nodes.Clear(); - setStatus("disconnected"); } - private void Form1_FormClosed(object sender, FormClosedEventArgs e) + private async void Form1_FormClosed(object sender, FormClosedEventArgs e) { - if (conn != null) conn.Disconnect(); + await CloseClientAsync().ConfigureAwait(true); } - private void treeView1_AfterExpand(object sender, TreeViewEventArgs e) + private async Task LoadBrowseTreeAsync() { - if (e.Node.Nodes.Count < 0 || e.Node.Nodes[0].Text != "Loading...") return; + treeView1.BeginUpdate(); + try + { + treeView1.Nodes.Clear(); - setStatus("loading..."); - e.Node.Nodes.Clear(); - uint relTiId = (uint)e.Node.Tag; - PObject pObj = conn.getTypeInfoByRelId(relTiId); - setStatus("connected"); + var variablesNode = treeView1.Nodes.Add("Variables"); + variablesNode.ImageKey = "Tag"; + variablesNode.SelectedImageKey = variablesNode.ImageKey; + + var blocksNode = treeView1.Nodes.Add("Blocks"); + blocksNode.ImageKey = "Datablock"; + blocksNode.SelectedImageKey = blocksNode.ImageKey; - if (pObj == null || pObj.VarnameList == null) return; - TreeNode tn; - TreeNode tnarr; - for (int i = 0; i < pObj.VarnameList.Names.Count; ++i) + await LoadBrowseTreeCoreAsync(variablesNode, blocksNode).ConfigureAwait(true); + } + finally { - tn = e.Node.Nodes.Add(pObj.VarnameList.Names[i]); - SetImageKey(ref tn, pObj.VartypeList.Elements[i]); - if (pObj.VartypeList.Elements[i].OffsetInfoType.Is1Dim()) - { - var ioitarr = (IOffsetInfoType_1Dim)pObj.VartypeList.Elements[i].OffsetInfoType; - uint arrayElementCount = ioitarr.GetArrayElementCount(); - int arrayLowerBounds = ioitarr.GetArrayLowerBounds(); - for (int j = 0; j < arrayElementCount; ++j) - { - tnarr = tn.Nodes.Add(pObj.VarnameList.Names[i] + "[" + (j + arrayLowerBounds) + "]"); - SetImageKey(ref tnarr, pObj.VartypeList.Elements[i]); - if (pObj.VartypeList.Elements[i].OffsetInfoType.HasRelation()) - { - var ioit = (IOffsetInfoType_Relation)pObj.VartypeList.Elements[i].OffsetInfoType; - tnarr.Nodes.Add("Loading..."); - tnarr.Tag = ioit.GetRelationId(); - SetImageKey(ref tnarr, pObj.VartypeList.Elements[i]); - } - } - tn.Tag = (uint)0; // is array - } - else if (pObj.VartypeList.Elements[i].OffsetInfoType.IsMDim()) - { - var ioitarrm = (IOffsetInfoType_MDim)pObj.VartypeList.Elements[i].OffsetInfoType; - uint[] MdimArrayElementCount = ioitarrm.GetMdimArrayElementCount(); - int[] MdimArrayLowerBounds = ioitarrm.GetMdimArrayLowerBounds(); - int dimCount = MdimArrayElementCount.Aggregate(0, (acc, act) => acc += (act > 0) ? 1 : 0); - int[] indexes = new int[dimCount]; - bool stop = false; - while (!stop) - { - string arrIdxStr = ""; - for (int j = dimCount - 1; j >= 0; --j) - { - arrIdxStr += (arrIdxStr != "" ? "," : "") + (indexes[j] + MdimArrayLowerBounds[j]); - } - tnarr = tn.Nodes.Add(pObj.VarnameList.Names[i] + "[" + arrIdxStr + "]"); - SetImageKey(ref tnarr, pObj.VartypeList.Elements[i]); - if (pObj.VartypeList.Elements[i].OffsetInfoType.HasRelation()) - { - var ioit = (IOffsetInfoType_Relation)pObj.VartypeList.Elements[i].OffsetInfoType; - tnarr.Nodes.Add("Loading..."); - tnarr.Tag = ioit.GetRelationId(); - SetImageKey(ref tnarr, pObj.VartypeList.Elements[i]); - } - ++indexes[0]; - for (int j = 0; j < dimCount; ++j) - { - if (indexes[j] >= MdimArrayElementCount[j]) - { - if (j + 1 < dimCount) - { - indexes[j] = 0; - ++indexes[j + 1]; - } - else - { - stop = true; - } - } - else - { - break; - } - } - } - tn.Tag = (uint)0; // is array - } - else - { - if (pObj.VartypeList.Elements[i].OffsetInfoType.HasRelation()) - { - var ioit = (IOffsetInfoType_Relation)pObj.VartypeList.Elements[i].OffsetInfoType; - tn.Nodes.Add("Loading..."); - tn.Tag = ioit.GetRelationId(); - SetImageKey(ref tn, pObj.VartypeList.Elements[i]); - } - } + treeView1.EndUpdate(); } } - private void SetImageKey(ref TreeNode tn, PVartypeListElement vte) + private async Task LoadBrowseTreeCoreAsync(TreeNode variablesNode, TreeNode blocksNode) { - string sk = "Tag"; - switch (vte.Softdatatype) + foreach (var variable in await client.BrowseAsync().ConfigureAwait(true)) { - case Softdatatype.S7COMMP_SOFTDATATYPE_BOOL: - case Softdatatype.S7COMMP_SOFTDATATYPE_BBOOL: - sk = "Boolean"; - break; - case Softdatatype.S7COMMP_SOFTDATATYPE_INT: - case Softdatatype.S7COMMP_SOFTDATATYPE_DINT: - case Softdatatype.S7COMMP_SOFTDATATYPE_ULINT: - case Softdatatype.S7COMMP_SOFTDATATYPE_LINT: - case Softdatatype.S7COMMP_SOFTDATATYPE_USINT: - case Softdatatype.S7COMMP_SOFTDATATYPE_UINT: - case Softdatatype.S7COMMP_SOFTDATATYPE_UDINT: - case Softdatatype.S7COMMP_SOFTDATATYPE_SINT: - // Derived types - case Softdatatype.S7COMMP_SOFTDATATYPE_BLOCKFB: - case Softdatatype.S7COMMP_SOFTDATATYPE_BLOCKFC: - case Softdatatype.S7COMMP_SOFTDATATYPE_OBANY: - case Softdatatype.S7COMMP_SOFTDATATYPE_OBDELAY: - case Softdatatype.S7COMMP_SOFTDATATYPE_OBTOD: - case Softdatatype.S7COMMP_SOFTDATATYPE_OBCYCLIC: - case Softdatatype.S7COMMP_SOFTDATATYPE_OBATT: - case Softdatatype.S7COMMP_SOFTDATATYPE_PORT: - case Softdatatype.S7COMMP_SOFTDATATYPE_RTM: - case Softdatatype.S7COMMP_SOFTDATATYPE_PIP: - case Softdatatype.S7COMMP_SOFTDATATYPE_OBPCYCLE: - case Softdatatype.S7COMMP_SOFTDATATYPE_OBHWINT: - case Softdatatype.S7COMMP_SOFTDATATYPE_OBDIAG: - case Softdatatype.S7COMMP_SOFTDATATYPE_OBTIMEERROR: - case Softdatatype.S7COMMP_SOFTDATATYPE_OBSTARTUP: - case Softdatatype.S7COMMP_SOFTDATATYPE_DBANY: - case Softdatatype.S7COMMP_SOFTDATATYPE_DBWWW: - case Softdatatype.S7COMMP_SOFTDATATYPE_DBDYN: - sk = "Integer2"; - break; - case Softdatatype.S7COMMP_SOFTDATATYPE_BYTE: - case Softdatatype.S7COMMP_SOFTDATATYPE_WORD: - case Softdatatype.S7COMMP_SOFTDATATYPE_DWORD: - case Softdatatype.S7COMMP_SOFTDATATYPE_LWORD: - // Derived types - case Softdatatype.S7COMMP_SOFTDATATYPE_AOMIDENT: - case Softdatatype.S7COMMP_SOFTDATATYPE_EVENTANY: - case Softdatatype.S7COMMP_SOFTDATATYPE_EVENTATT: - case Softdatatype.S7COMMP_SOFTDATATYPE_AOMAID: - case Softdatatype.S7COMMP_SOFTDATATYPE_AOMLINK: - case Softdatatype.S7COMMP_SOFTDATATYPE_EVENTHWINT: - case Softdatatype.S7COMMP_SOFTDATATYPE_HWANY: - case Softdatatype.S7COMMP_SOFTDATATYPE_HWIOSYSTEM: - case Softdatatype.S7COMMP_SOFTDATATYPE_HWDPMASTER: - case Softdatatype.S7COMMP_SOFTDATATYPE_HWDEVICE: - case Softdatatype.S7COMMP_SOFTDATATYPE_HWDPSLAVE: - case Softdatatype.S7COMMP_SOFTDATATYPE_HWIO: - case Softdatatype.S7COMMP_SOFTDATATYPE_HWMODULE: - case Softdatatype.S7COMMP_SOFTDATATYPE_HWSUBMODULE: - case Softdatatype.S7COMMP_SOFTDATATYPE_HWHSC: - case Softdatatype.S7COMMP_SOFTDATATYPE_HWPWM: - case Softdatatype.S7COMMP_SOFTDATATYPE_HWPTO: - case Softdatatype.S7COMMP_SOFTDATATYPE_HWINTERFACE: - case Softdatatype.S7COMMP_SOFTDATATYPE_HWIEPORT: - case Softdatatype.S7COMMP_SOFTDATATYPE_CONNANY: - case Softdatatype.S7COMMP_SOFTDATATYPE_CONNPRG: - case Softdatatype.S7COMMP_SOFTDATATYPE_CONNOUC: - case Softdatatype.S7COMMP_SOFTDATATYPE_CONNRID: - sk = "Binary2"; - break; - case Softdatatype.S7COMMP_SOFTDATATYPE_REAL: - case Softdatatype.S7COMMP_SOFTDATATYPE_LREAL: - sk = "Number2"; - break; - case Softdatatype.S7COMMP_SOFTDATATYPE_CHAR: - case Softdatatype.S7COMMP_SOFTDATATYPE_WCHAR: - sk = "Char"; - break; - case Softdatatype.S7COMMP_SOFTDATATYPE_STRING: - case Softdatatype.S7COMMP_SOFTDATATYPE_WSTRING: - sk = "Text"; - break; - case Softdatatype.S7COMMP_SOFTDATATYPE_DATE: - sk = "Date"; - break; - case Softdatatype.S7COMMP_SOFTDATATYPE_TIMEOFDAY: - case Softdatatype.S7COMMP_SOFTDATATYPE_LTOD: - sk = "Time"; - break; - case Softdatatype.S7COMMP_SOFTDATATYPE_TIME: - case Softdatatype.S7COMMP_SOFTDATATYPE_LTIME: - case Softdatatype.S7COMMP_SOFTDATATYPE_S5TIME: - case Softdatatype.S7COMMP_SOFTDATATYPE_TIMER: - sk = "Timer"; // Duration - break; - case Softdatatype.S7COMMP_SOFTDATATYPE_DATEANDTIME: - case Softdatatype.S7COMMP_SOFTDATATYPE_LDT: - case Softdatatype.S7COMMP_SOFTDATATYPE_DTL: - sk = "DateTime"; - break; - case Softdatatype.S7COMMP_SOFTDATATYPE_ANY: - case Softdatatype.S7COMMP_SOFTDATATYPE_POINTER: - case Softdatatype.S7COMMP_SOFTDATATYPE_REMOTE: - sk = "Any"; - break; + var node = variablesNode.Nodes.Add(variable.Name); + node.Tag = variable; + node.ImageKey = "Tag"; + node.SelectedImageKey = node.ImageKey; } - if (vte.OffsetInfoType.HasRelation()) + + foreach (var root in await client.BrowseBlockStructureAsync().ConfigureAwait(true)) { - sk = "Structure"; + blocksNode.Nodes.Add(CreateStructureNode(root)); } - tn.ImageKey = sk; - tn.SelectedImageKey = tn.ImageKey; + + variablesNode.Expand(); + blocksNode.Expand(); } - private string escapeTiaString(string str, bool isRootNode, bool isArray) + private static TreeNode CreateStructureNode(S7CommPlusPlcStructureNode structureNode) { - if (isRootNode) return '"' + str + '"'; - Regex re = new Regex("(^[0-9]|[^0-9A-Za-z_])"); - if (isArray) + var text = structureNode.Number.HasValue + ? $"{structureNode.BlockType ?? structureNode.Kind.ToString()} {structureNode.Number}: {structureNode.Name}" + : structureNode.Name; + + var node = new TreeNode(text) { - Regex reArr = new Regex("^([^\"]*)(\\[[0-9, ]+\\])$"); - Match m = reArr.Match(str); - if (!m.Success) return str; - if (re.Match(m.Groups[1].Value).Success) return '"' + m.Groups[1].Value + '"' + m.Groups[2].Value; - return str; + Tag = structureNode, + ImageKey = structureNode.Kind == S7CommPlusPlcStructureNodeKind.Block ? "Datablock" : "Default" + }; + node.SelectedImageKey = node.ImageKey; + + foreach (var child in structureNode.Children) + { + node.Nodes.Add(CreateStructureNode(child)); } - if (re.IsMatch(str)) return '"' + str + '"'; - return str; + + return node; } - private void readTagBySymbol() + private async Task CloseClientAsync() { - tbValue.Text = ""; - tbSymbolicAddress.Text = ""; + if (client == null) + { + return; + } - setStatus("loading..."); - PlcTag tag = conn.getPlcTagBySymbol(tbSymbol.Text); - setStatus("connected"); - if (tag == null) return; + var oldClient = client; + client = null; + await oldClient.DisposeAsync().ConfigureAwait(true); + } + + private async Task ReadTagBySymbolAsync() + { + tbValue.Text = string.Empty; + tbSymbolicAddress.Text = string.Empty; + + if (client == null || string.IsNullOrWhiteSpace(tbSymbol.Text)) + { + return; + } + setStatus("loading..."); + var tag = await client.GetTagBySymbolAsync(tbSymbol.Text).ConfigureAwait(true); tbSymbolicAddress.Text = tag.Address.GetAccessString(); - if (conn.ReadTags(new[] { tag }) != 0) return; + await client.ReadAsync(new[] { tag }).ConfigureAwait(true); tbValue.Text = tag.ToString(); + setStatus("connected"); } - private void treeView1_AfterSelect(object sender, TreeViewEventArgs e) + private async void treeView1_AfterSelect(object sender, TreeViewEventArgs e) { - if (e.Node.Tag != null) return; // has relId - - string name = ""; - TreeNode tn = e.Node; - while (tn != null) + if (e.Node.Tag is not VarInfo variable) { - bool isArray = false; - string nodeText = tn.Text; - tn = tn.Parent; - if (tn != null && tn.Tag != null) - { // is array - if ((uint)tn.Tag == 0) - { - isArray = true; - tn = tn.Parent; // skip array parent - } - } - if (tn != null && tn.Tag != null) - { // don't add in/out/merker area as tag - uint relId = (uint)tn.Tag; - if (relId == 0x90010000 || relId == 0x90020000 || relId == 0x90030000) tn = null; - } - name = escapeTiaString(nodeText, tn == null, isArray) + (name != "" ? "." : "") + name; + return; } - tbSymbol.Text = name; - readTagBySymbol(); + tbSymbol.Text = variable.Name; + try + { + await ReadTagBySymbolAsync().ConfigureAwait(true); + } + catch (S7CommPlusException ex) + { + setStatus("error: " + ex.Message); + } } - private void btnRead_Click(object sender, EventArgs e) + private void treeView1_AfterExpand(object sender, TreeViewEventArgs e) { - if (tbSymbol.Text == "") return; + } + private async void btnRead_Click(object sender, EventArgs e) + { try { - readTagBySymbol(); + await ReadTagBySymbolAsync().ConfigureAwait(true); } catch (Exception ex) { diff --git a/src/S7CommPlusGUIBrowser/Properties/AssemblyInfo.cs b/src/S7CommPlusGUIBrowser/Properties/AssemblyInfo.cs deleted file mode 100644 index df3272c..0000000 --- a/src/S7CommPlusGUIBrowser/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// Allgemeine Informationen über eine Assembly werden über die folgenden -// Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, -// die einer Assembly zugeordnet sind. -[assembly: AssemblyTitle("S7CommPlusGUIBrowser")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("S7CommPlusGUIBrowser")] -[assembly: AssemblyCopyright("Copyright © 2024")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Durch Festlegen von ComVisible auf FALSE werden die Typen in dieser Assembly -// für COM-Komponenten unsichtbar. Wenn Sie auf einen Typ in dieser Assembly von -// COM aus zugreifen müssen, sollten Sie das ComVisible-Attribut für diesen Typ auf "True" festlegen. -[assembly: ComVisible(false)] - -// Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird -[assembly: Guid("beb37276-b3b3-4dee-a1ea-d95eb774223b")] - -// Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten: -// -// Hauptversion -// Nebenversion -// Buildnummer -// Revision -// -// Sie können alle Werte angeben oder Standardwerte für die Build- und Revisionsnummern verwenden, -// indem Sie "*" wie unten gezeigt eingeben: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/S7CommPlusGUIBrowser/Properties/Resources.Designer.cs b/src/S7CommPlusGUIBrowser/Properties/Resources.Designer.cs deleted file mode 100644 index 869a851..0000000 --- a/src/S7CommPlusGUIBrowser/Properties/Resources.Designer.cs +++ /dev/null @@ -1,62 +0,0 @@ -//------------------------------------------------------------------------------ -// -// Dieser Code wurde von einem Tool generiert. -// Laufzeitversion: 4.0.30319.42000 -// -// Änderungen an dieser Datei können fehlerhaftes Verhalten verursachen und gehen verloren, wenn -// der Code neu generiert wird. -// -//------------------------------------------------------------------------------ - -namespace S7CommPlusGUIBrowser.Properties { - - - /// - /// Eine stark typisierte Ressourcenklasse zum Suchen von lokalisierten Zeichenfolgen usw. - /// - // Diese Klasse wurde von der StronglyTypedResourceBuilder-Klasse - // über ein Tool wie ResGen oder Visual Studio automatisch generiert. - // Um einen Member hinzuzufügen oder zu entfernen, bearbeiten Sie die .ResX-Datei und führen dann ResGen - // mit der Option /str erneut aus, oder erstellen Sie Ihr VS-Projekt neu. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Gibt die zwischengespeicherte ResourceManager-Instanz zurück, die von dieser Klasse verwendet wird. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if ((resourceMan == null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("S7CommPlusGUIBrowser.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Überschreibt die CurrentUICulture-Eigenschaft des aktuellen Threads für alle - /// Ressourcenlookups, die diese stark typisierte Ressourcenklasse verwenden. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - } -} diff --git a/src/S7CommPlusGUIBrowser/Properties/Resources.resx b/src/S7CommPlusGUIBrowser/Properties/Resources.resx deleted file mode 100644 index ffecec8..0000000 --- a/src/S7CommPlusGUIBrowser/Properties/Resources.resx +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/src/S7CommPlusGUIBrowser/Properties/Settings.Designer.cs b/src/S7CommPlusGUIBrowser/Properties/Settings.Designer.cs deleted file mode 100644 index e62375c..0000000 --- a/src/S7CommPlusGUIBrowser/Properties/Settings.Designer.cs +++ /dev/null @@ -1,26 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace S7CommPlusGUIBrowser.Properties { - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default { - get { - return defaultInstance; - } - } - } -} diff --git a/src/S7CommPlusGUIBrowser/Properties/Settings.settings b/src/S7CommPlusGUIBrowser/Properties/Settings.settings deleted file mode 100644 index abf36c5..0000000 --- a/src/S7CommPlusGUIBrowser/Properties/Settings.settings +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/src/S7CommPlusGUIBrowser/S7CommPlusGUIBrowser.csproj b/src/S7CommPlusGUIBrowser/S7CommPlusGUIBrowser.csproj index 1d43460..bf88170 100644 --- a/src/S7CommPlusGUIBrowser/S7CommPlusGUIBrowser.csproj +++ b/src/S7CommPlusGUIBrowser/S7CommPlusGUIBrowser.csproj @@ -1,114 +1,21 @@ - - - - - Debug - AnyCPU - {BEB37276-B3B3-4DEE-A1EA-D95EB774223B} - WinExe - S7CommPlusGUIBrowser - S7CommPlusGUIBrowser - v4.7.2 - 512 - true - true + + + net8.0-windows + WinExe + false + true + true + true + false - - true - bin\x64\Debug\ - DEBUG;TRACE - full - x64 - 7.3 - prompt - true - - - bin\x64\Release\ - TRACE - true - pdbonly - x64 - 7.3 - prompt - true - - - true - bin\x86\Debug\ - DEBUG;TRACE - full - x86 - 7.3 - prompt - true - - - bin\x86\Release\ - TRACE - true - pdbonly - x86 - 7.3 - prompt - true - - - - - - - - - - - - - - - - - Form - - - Form1.cs - - - - - Form1.cs - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - - - True - Resources.resx - - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - True - Settings.settings - True - - - - - - - - {781951d8-80de-4e2f-92ca-a5fef0059415} - S7CommPlusDriver - - - - - xcopy /y /d "$(SolutionDir)S7CommPlusDriver\OpenSSL-dll-$(PlatformName)\*.dll" "$(ProjectDir)$(OutDir)" - - \ No newline at end of file + + 1701;1702;CA1416 + + + + + + + + + diff --git a/src/Zlib.net/Adler32.cs b/src/Zlib.net/Adler32.cs deleted file mode 100644 index 6b9fd62..0000000 --- a/src/Zlib.net/Adler32.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) 2006, ComponentAce -// http://www.componentace.com -// All rights reserved. - -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -// Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -// Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -// Neither the name of ComponentAce nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - - -/* -Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -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. The names of the authors may not be used to endorse or promote products -derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 JCRAFT, -INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -/* -* This program is based on zlib-1.1.3, so all credit should go authors -* Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) -* and contributors of zlib. -*/ -using System; -namespace ComponentAce.Compression.Libs.zlib -{ - - sealed class Adler32 - { - - // largest prime smaller than 65536 - private const int BASE = 65521; - // NMAX is the largest n such that 255n(n+1)/2 + (n+1)(BASE-1) <= 2^32-1 - private const int NMAX = 5552; - - internal long adler32(long adler, byte[] buf, int index, int len) - { - if (buf == null) - { - return 1L; - } - - long s1 = adler & 0xffff; - long s2 = (adler >> 16) & 0xffff; - int k; - - while (len > 0) - { - k = len < NMAX?len:NMAX; - len -= k; - while (k >= 16) - { - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - s1 += (buf[index++] & 0xff); s2 += s1; - k -= 16; - } - if (k != 0) - { - do - { - s1 += (buf[index++] & 0xff); s2 += s1; - } - while (--k != 0); - } - s1 %= BASE; - s2 %= BASE; - } - return (s2 << 16) | s1; - } - - } -} \ No newline at end of file diff --git a/src/Zlib.net/AssemblyInfo.cs b/src/Zlib.net/AssemblyInfo.cs deleted file mode 100644 index 1faa426..0000000 --- a/src/Zlib.net/AssemblyInfo.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; - -// -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -// -[assembly: AssemblyTitle("ZLIB.NET")] -[assembly: AssemblyDescription("ZLIB.NET")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("ComponentAce")] -[assembly: AssemblyProduct("ZLIB.NET")] -[assembly: AssemblyCopyright("ComponentAce")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Revision and Build Numbers -// by using the '*' as shown below: - -[assembly: AssemblyVersion("1.0.3.0")] - -// -// In order to sign your assembly you must specify a key to use. Refer to the -// Microsoft .NET Framework documentation for more information on assembly signing. -// -// Use the attributes below to control which key is used for signing. -// -// Notes: -// (*) If no key is specified, the assembly is not signed. -// (*) KeyName refers to a key that has been installed in the Crypto Service -// Provider (CSP) on your machine. KeyFile refers to a file which contains -// a key. -// (*) If the KeyFile and the KeyName values are both specified, the -// following processing occurs: -// (1) If the KeyName can be found in the CSP, that key is used. -// (2) If the KeyName does not exist and the KeyFile does exist, the key -// in the KeyFile is installed into the CSP and used. -// (*) In order to create a KeyFile, you can use the sn.exe (Strong Name) utility. -// When specifying the KeyFile, the location of the KeyFile should be -// relative to the project output directory which is -// %Project Directory%\obj\. For example, if your KeyFile is -// located in the project directory, you would specify the AssemblyKeyFile -// attribute as [assembly: AssemblyKeyFile("..\\..\\mykey.snk")] -// (*) Delay Signing is an advanced option - see the Microsoft .NET Framework -// documentation for more information on this. -// -[assembly: AssemblyDelaySign(false)] -[assembly: AssemblyKeyFile("")] -[assembly: AssemblyKeyName("")] diff --git a/src/Zlib.net/Deflate.cs b/src/Zlib.net/Deflate.cs deleted file mode 100644 index 24a25d3..0000000 --- a/src/Zlib.net/Deflate.cs +++ /dev/null @@ -1,1799 +0,0 @@ -// Copyright (c) 2006, ComponentAce -// http://www.componentace.com -// All rights reserved. - -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -// Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -// Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -// Neither the name of ComponentAce nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -/* -Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -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. The names of the authors may not be used to endorse or promote products -derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 JCRAFT, -INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -/* -* This program is based on zlib-1.1.3, so all credit should go authors -* Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) -* and contributors of zlib. -*/ -using System; -namespace ComponentAce.Compression.Libs.zlib -{ - - public sealed class Deflate - { - - private const int MAX_MEM_LEVEL = 9; - - private const int Z_DEFAULT_COMPRESSION = - 1; - - private const int MAX_WBITS = 15; // 32K LZ77 window - private const int DEF_MEM_LEVEL = 8; - - internal class Config - { - internal int good_length; // reduce lazy search above this match length - internal int max_lazy; // do not perform lazy search above this match length - internal int nice_length; // quit search above this match length - internal int max_chain; - internal int func; - internal Config(int good_length, int max_lazy, int nice_length, int max_chain, int func) - { - this.good_length = good_length; - this.max_lazy = max_lazy; - this.nice_length = nice_length; - this.max_chain = max_chain; - this.func = func; - } - } - - private const int STORED = 0; - private const int FAST = 1; - private const int SLOW = 2; - private static Config[] config_table; - - private static readonly System.String[] z_errmsg = new System.String[]{"need dictionary", "stream end", "", "file error", "stream error", "data error", "insufficient memory", "buffer error", "incompatible version", ""}; - - // block not completed, need more input or more output - private const int NeedMore = 0; - - // block flush performed - private const int BlockDone = 1; - - // finish started, need only more output at next deflate - private const int FinishStarted = 2; - - // finish done, accept no more input or output - private const int FinishDone = 3; - - // preset dictionary flag in zlib header - private const int PRESET_DICT = 0x20; - - private const int Z_FILTERED = 1; - private const int Z_HUFFMAN_ONLY = 2; - private const int Z_DEFAULT_STRATEGY = 0; - - private const int Z_NO_FLUSH = 0; - private const int Z_PARTIAL_FLUSH = 1; - private const int Z_SYNC_FLUSH = 2; - private const int Z_FULL_FLUSH = 3; - private const int Z_FINISH = 4; - - private const int Z_OK = 0; - private const int Z_STREAM_END = 1; - private const int Z_NEED_DICT = 2; - private const int Z_ERRNO = - 1; - private const int Z_STREAM_ERROR = - 2; - private const int Z_DATA_ERROR = - 3; - private const int Z_MEM_ERROR = - 4; - private const int Z_BUF_ERROR = - 5; - private const int Z_VERSION_ERROR = - 6; - - private const int INIT_STATE = 42; - private const int BUSY_STATE = 113; - private const int FINISH_STATE = 666; - - // The deflate compression method - private const int Z_DEFLATED = 8; - - private const int STORED_BLOCK = 0; - private const int STATIC_TREES = 1; - private const int DYN_TREES = 2; - - // The three kinds of block type - private const int Z_BINARY = 0; - private const int Z_ASCII = 1; - private const int Z_UNKNOWN = 2; - - private const int Buf_size = 8 * 2; - - // repeat previous bit length 3-6 times (2 bits of repeat count) - private const int REP_3_6 = 16; - - // repeat a zero length 3-10 times (3 bits of repeat count) - private const int REPZ_3_10 = 17; - - // repeat a zero length 11-138 times (7 bits of repeat count) - private const int REPZ_11_138 = 18; - - private const int MIN_MATCH = 3; - private const int MAX_MATCH = 258; - private static readonly int MIN_LOOKAHEAD = (MAX_MATCH + MIN_MATCH + 1); - - private const int MAX_BITS = 15; - private const int D_CODES = 30; - private const int BL_CODES = 19; - private const int LENGTH_CODES = 29; - private const int LITERALS = 256; - private static readonly int L_CODES = (LITERALS + 1 + LENGTH_CODES); - private static readonly int HEAP_SIZE = (2 * L_CODES + 1); - - private const int END_BLOCK = 256; - - internal ZStream strm; // pointer back to this zlib stream - internal int status; // as the name implies - internal byte[] pending_buf; // output still pending - internal int pending_buf_size; // size of pending_buf - internal int pending_out; // next pending byte to output to the stream - internal int pending; // nb of bytes in the pending buffer - internal int noheader; // suppress zlib header and adler32 - internal byte data_type; // UNKNOWN, BINARY or ASCII - internal byte method; // STORED (for zip only) or DEFLATED - internal int last_flush; // value of flush param for previous deflate call - - internal int w_size; // LZ77 window size (32K by default) - internal int w_bits; // log2(w_size) (8..16) - internal int w_mask; // w_size - 1 - - internal byte[] window; - // Sliding window. Input bytes are read into the second half of the window, - // and move to the first half later to keep a dictionary of at least wSize - // bytes. With this organization, matches are limited to a distance of - // wSize-MAX_MATCH bytes, but this ensures that IO is always - // performed with a length multiple of the block size. Also, it limits - // the window size to 64K, which is quite useful on MSDOS. - // To do: use the user input buffer as sliding window. - - internal int window_size; - // Actual size of window: 2*wSize, except when the user input buffer - // is directly used as sliding window. - - internal short[] prev; - // Link to older string with same hash index. To limit the size of this - // array to 64K, this link is maintained only for the last 32K strings. - // An index in this array is thus a window index modulo 32K. - - internal short[] head; // Heads of the hash chains or NIL. - - internal int ins_h; // hash index of string to be inserted - internal int hash_size; // number of elements in hash table - internal int hash_bits; // log2(hash_size) - internal int hash_mask; // hash_size-1 - - // Number of bits by which ins_h must be shifted at each input - // step. It must be such that after MIN_MATCH steps, the oldest - // byte no longer takes part in the hash key, that is: - // hash_shift * MIN_MATCH >= hash_bits - internal int hash_shift; - - // Window position at the beginning of the current output block. Gets - // negative when the window is moved backwards. - - internal int block_start; - - internal int match_length; // length of best match - internal int prev_match; // previous match - internal int match_available; // set if previous match exists - internal int strstart; // start of string to insert - internal int match_start; // start of matching string - internal int lookahead; // number of valid bytes ahead in window - - // Length of the best match at previous step. Matches not greater than this - // are discarded. This is used in the lazy match evaluation. - internal int prev_length; - - // To speed up deflation, hash chains are never searched beyond this - // length. A higher limit improves compression ratio but degrades the speed. - internal int max_chain_length; - - // Attempt to find a better match only when the current match is strictly - // smaller than this value. This mechanism is used only for compression - // levels >= 4. - internal int max_lazy_match; - - // Insert new strings in the hash table only if the match length is not - // greater than this length. This saves time but degrades compression. - // max_insert_length is used only for compression levels <= 3. - - internal int level; // compression level (1..9) - internal int strategy; // favor or force Huffman coding - - // Use a faster search when the previous match is longer than this - internal int good_match; - - // Stop searching when current match exceeds this - internal int nice_match; - - internal short[] dyn_ltree; // literal and length tree - internal short[] dyn_dtree; // distance tree - internal short[] bl_tree; // Huffman tree for bit lengths - - internal Tree l_desc = new Tree(); // desc for literal tree - internal Tree d_desc = new Tree(); // desc for distance tree - internal Tree bl_desc = new Tree(); // desc for bit length tree - - // number of codes at each bit length for an optimal tree - internal short[] bl_count = new short[MAX_BITS + 1]; - - // heap used to build the Huffman trees - internal int[] heap = new int[2 * L_CODES + 1]; - - internal int heap_len; // number of elements in the heap - internal int heap_max; // element of largest frequency - // The sons of heap[n] are heap[2*n] and heap[2*n+1]. heap[0] is not used. - // The same heap array is used to build all trees. - - // Depth of each subtree used as tie breaker for trees of equal frequency - internal byte[] depth = new byte[2 * L_CODES + 1]; - - internal int l_buf; // index for literals or lengths */ - - // Size of match buffer for literals/lengths. There are 4 reasons for - // limiting lit_bufsize to 64K: - // - frequencies can be kept in 16 bit counters - // - if compression is not successful for the first block, all input - // data is still in the window so we can still emit a stored block even - // when input comes from standard input. (This can also be done for - // all blocks if lit_bufsize is not greater than 32K.) - // - if compression is not successful for a file smaller than 64K, we can - // even emit a stored file instead of a stored block (saving 5 bytes). - // This is applicable only for zip (not gzip or zlib). - // - creating new Huffman trees less frequently may not provide fast - // adaptation to changes in the input data statistics. (Take for - // example a binary file with poorly compressible code followed by - // a highly compressible string table.) Smaller buffer sizes give - // fast adaptation but have of course the overhead of transmitting - // trees more frequently. - // - I can't count above 4 - internal int lit_bufsize; - - internal int last_lit; // running index in l_buf - - // Buffer for distances. To simplify the code, d_buf and l_buf have - // the same number of elements. To use different lengths, an extra flag - // array would be necessary. - - internal int d_buf; // index of pendig_buf - - internal int opt_len; // bit length of current block with optimal trees - internal int static_len; // bit length of current block with static trees - internal int matches; // number of string matches in current block - internal int last_eob_len; // bit length of EOB code for last block - - // Output buffer. bits are inserted starting at the bottom (least - // significant bits). - internal short bi_buf; - - // Number of valid bits in bi_buf. All bits above the last valid bit - // are always zero. - internal int bi_valid; - - internal Deflate() - { - dyn_ltree = new short[HEAP_SIZE * 2]; - dyn_dtree = new short[(2 * D_CODES + 1) * 2]; // distance tree - bl_tree = new short[(2 * BL_CODES + 1) * 2]; // Huffman tree for bit lengths - } - - internal void lm_init() - { - window_size = 2 * w_size; - - head[hash_size - 1] = 0; - for (int i = 0; i < hash_size - 1; i++) - { - head[i] = 0; - } - - // Set the default configuration parameters: - max_lazy_match = Deflate.config_table[level].max_lazy; - good_match = Deflate.config_table[level].good_length; - nice_match = Deflate.config_table[level].nice_length; - max_chain_length = Deflate.config_table[level].max_chain; - - strstart = 0; - block_start = 0; - lookahead = 0; - match_length = prev_length = MIN_MATCH - 1; - match_available = 0; - ins_h = 0; - } - - // Initialize the tree data structures for a new zlib stream. - internal void tr_init() - { - - l_desc.dyn_tree = dyn_ltree; - l_desc.stat_desc = StaticTree.static_l_desc; - - d_desc.dyn_tree = dyn_dtree; - d_desc.stat_desc = StaticTree.static_d_desc; - - bl_desc.dyn_tree = bl_tree; - bl_desc.stat_desc = StaticTree.static_bl_desc; - - bi_buf = 0; - bi_valid = 0; - last_eob_len = 8; // enough lookahead for inflate - - // Initialize the first block of the first file: - init_block(); - } - - internal void init_block() - { - // Initialize the trees. - for (int i = 0; i < L_CODES; i++) - dyn_ltree[i * 2] = 0; - for (int i = 0; i < D_CODES; i++) - dyn_dtree[i * 2] = 0; - for (int i = 0; i < BL_CODES; i++) - bl_tree[i * 2] = 0; - - dyn_ltree[END_BLOCK * 2] = 1; - opt_len = static_len = 0; - last_lit = matches = 0; - } - - // Restore the heap property by moving down the tree starting at node k, - // exchanging a node with the smallest of its two sons if necessary, stopping - // when the heap property is re-established (each father smaller than its - // two sons). - internal void pqdownheap(short[] tree, int k) - { - int v = heap[k]; - int j = k << 1; // left son of k - while (j <= heap_len) - { - // Set j to the smallest of the two sons: - if (j < heap_len && smaller(tree, heap[j + 1], heap[j], depth)) - { - j++; - } - // Exit if v is smaller than both sons - if (smaller(tree, v, heap[j], depth)) - break; - - // Exchange v with the smallest son - heap[k] = heap[j]; k = j; - // And continue down the tree, setting j to the left son of k - j <<= 1; - } - heap[k] = v; - } - - internal static bool smaller(short[] tree, int n, int m, byte[] depth) - { - return (tree[n * 2] < tree[m * 2] || (tree[n * 2] == tree[m * 2] && depth[n] <= depth[m])); - } - - // Scan a literal or distance tree to determine the frequencies of the codes - // in the bit length tree. - internal void scan_tree(short[] tree, int max_code) - { - int n; // iterates over all tree elements - int prevlen = - 1; // last emitted length - int curlen; // length of current code - int nextlen = tree[0 * 2 + 1]; // length of next code - int count = 0; // repeat count of the current code - int max_count = 7; // max repeat count - int min_count = 4; // min repeat count - - if (nextlen == 0) - { - max_count = 138; min_count = 3; - } - tree[(max_code + 1) * 2 + 1] = (short) SupportClass.Identity(0xffff); // guard - - for (n = 0; n <= max_code; n++) - { - curlen = nextlen; nextlen = tree[(n + 1) * 2 + 1]; - if (++count < max_count && curlen == nextlen) - { - continue; - } - else if (count < min_count) - { - bl_tree[curlen * 2] = (short) (bl_tree[curlen * 2] + count); - } - else if (curlen != 0) - { - if (curlen != prevlen) - bl_tree[curlen * 2]++; - bl_tree[REP_3_6 * 2]++; - } - else if (count <= 10) - { - bl_tree[REPZ_3_10 * 2]++; - } - else - { - bl_tree[REPZ_11_138 * 2]++; - } - count = 0; prevlen = curlen; - if (nextlen == 0) - { - max_count = 138; min_count = 3; - } - else if (curlen == nextlen) - { - max_count = 6; min_count = 3; - } - else - { - max_count = 7; min_count = 4; - } - } - } - - // Construct the Huffman tree for the bit lengths and return the index in - // bl_order of the last bit length code to send. - internal int build_bl_tree() - { - int max_blindex; // index of last bit length code of non zero freq - - // Determine the bit length frequencies for literal and distance trees - scan_tree(dyn_ltree, l_desc.max_code); - scan_tree(dyn_dtree, d_desc.max_code); - - // Build the bit length tree: - bl_desc.build_tree(this); - // opt_len now includes the length of the tree representations, except - // the lengths of the bit lengths codes and the 5+5+4 bits for the counts. - - // Determine the number of bit length codes to send. The pkzip format - // requires that at least 4 bit length codes be sent. (appnote.txt says - // 3 but the actual value used is 4.) - for (max_blindex = BL_CODES - 1; max_blindex >= 3; max_blindex--) - { - if (bl_tree[Tree.bl_order[max_blindex] * 2 + 1] != 0) - break; - } - // Update opt_len to include the bit length tree and counts - opt_len += 3 * (max_blindex + 1) + 5 + 5 + 4; - - return max_blindex; - } - - - // Send the header for a block using dynamic Huffman trees: the counts, the - // lengths of the bit length codes, the literal tree and the distance tree. - // IN assertion: lcodes >= 257, dcodes >= 1, blcodes >= 4. - internal void send_all_trees(int lcodes, int dcodes, int blcodes) - { - int rank; // index in bl_order - - send_bits(lcodes - 257, 5); // not +255 as stated in appnote.txt - send_bits(dcodes - 1, 5); - send_bits(blcodes - 4, 4); // not -3 as stated in appnote.txt - for (rank = 0; rank < blcodes; rank++) - { - send_bits(bl_tree[Tree.bl_order[rank] * 2 + 1], 3); - } - send_tree(dyn_ltree, lcodes - 1); // literal tree - send_tree(dyn_dtree, dcodes - 1); // distance tree - } - - // Send a literal or distance tree in compressed form, using the codes in - // bl_tree. - internal void send_tree(short[] tree, int max_code) - { - int n; // iterates over all tree elements - int prevlen = - 1; // last emitted length - int curlen; // length of current code - int nextlen = tree[0 * 2 + 1]; // length of next code - int count = 0; // repeat count of the current code - int max_count = 7; // max repeat count - int min_count = 4; // min repeat count - - if (nextlen == 0) - { - max_count = 138; min_count = 3; - } - - for (n = 0; n <= max_code; n++) - { - curlen = nextlen; nextlen = tree[(n + 1) * 2 + 1]; - if (++count < max_count && curlen == nextlen) - { - continue; - } - else if (count < min_count) - { - do - { - send_code(curlen, bl_tree); - } - while (--count != 0); - } - else if (curlen != 0) - { - if (curlen != prevlen) - { - send_code(curlen, bl_tree); count--; - } - send_code(REP_3_6, bl_tree); - send_bits(count - 3, 2); - } - else if (count <= 10) - { - send_code(REPZ_3_10, bl_tree); - send_bits(count - 3, 3); - } - else - { - send_code(REPZ_11_138, bl_tree); - send_bits(count - 11, 7); - } - count = 0; prevlen = curlen; - if (nextlen == 0) - { - max_count = 138; min_count = 3; - } - else if (curlen == nextlen) - { - max_count = 6; min_count = 3; - } - else - { - max_count = 7; min_count = 4; - } - } - } - - // Output a byte on the stream. - // IN assertion: there is enough room in pending_buf. - internal void put_byte(byte[] p, int start, int len) - { - Array.Copy(p, start, pending_buf, pending, len); - pending += len; - } - - internal void put_byte(byte c) - { - pending_buf[pending++] = c; - } - internal void put_short(int w) - { - put_byte((byte) (w)); - put_byte((byte) (SupportClass.URShift(w, 8))); - } - internal void putShortMSB(int b) - { - put_byte((byte) (b >> 8)); - put_byte((byte) (b)); - } - - internal void send_code(int c, short[] tree) - { - send_bits((tree[c * 2] & 0xffff), (tree[c * 2 + 1] & 0xffff)); - } - - internal void send_bits(int value_Renamed, int length) - { - int len = length; - if (bi_valid > (int) Buf_size - len) - { - int val = value_Renamed; - // bi_buf |= (val << bi_valid); - bi_buf = (short) ((ushort) bi_buf | (ushort) (((val << bi_valid) & 0xffff))); - put_short(bi_buf); - bi_buf = (short) (SupportClass.URShift(val, (Buf_size - bi_valid))); - bi_valid += len - Buf_size; - } - else - { - // bi_buf |= (value) << bi_valid; - bi_buf = (short)((ushort)bi_buf | (ushort)((((value_Renamed) << bi_valid) & 0xffff))); - bi_valid += len; - } - } - - // Send one empty static block to give enough lookahead for inflate. - // This takes 10 bits, of which 7 may remain in the bit buffer. - // The current inflate code requires 9 bits of lookahead. If the - // last two codes for the previous block (real code plus EOB) were coded - // on 5 bits or less, inflate may have only 5+3 bits of lookahead to decode - // the last real code. In this case we send two empty static blocks instead - // of one. (There are no problems if the previous block is stored or fixed.) - // To simplify the code, we assume the worst case of last real code encoded - // on one bit only. - internal void _tr_align() - { - send_bits(STATIC_TREES << 1, 3); - send_code(END_BLOCK, StaticTree.static_ltree); - - bi_flush(); - - // Of the 10 bits for the empty block, we have already sent - // (10 - bi_valid) bits. The lookahead for the last real code (before - // the EOB of the previous block) was thus at least one plus the length - // of the EOB plus what we have just sent of the empty static block. - if (1 + last_eob_len + 10 - bi_valid < 9) - { - send_bits(STATIC_TREES << 1, 3); - send_code(END_BLOCK, StaticTree.static_ltree); - bi_flush(); - } - last_eob_len = 7; - } - - - // Save the match info and tally the frequency counts. Return true if - // the current block must be flushed. - internal bool _tr_tally(int dist, int lc) - { - - pending_buf[d_buf + last_lit * 2] = (byte) (SupportClass.URShift(dist, 8)); - pending_buf[d_buf + last_lit * 2 + 1] = (byte) dist; - - pending_buf[l_buf + last_lit] = (byte) lc; last_lit++; - - if (dist == 0) - { - // lc is the unmatched char - dyn_ltree[lc * 2]++; - } - else - { - matches++; - // Here, lc is the match length - MIN_MATCH - dist--; // dist = match distance - 1 - dyn_ltree[(Tree._length_code[lc] + LITERALS + 1) * 2]++; - dyn_dtree[Tree.d_code(dist) * 2]++; - } - - if ((last_lit & 0x1fff) == 0 && level > 2) - { - // Compute an upper bound for the compressed length - int out_length = last_lit * 8; - int in_length = strstart - block_start; - int dcode; - for (dcode = 0; dcode < D_CODES; dcode++) - { - out_length = (int) (out_length + (int) dyn_dtree[dcode * 2] * (5L + Tree.extra_dbits[dcode])); - } - out_length = SupportClass.URShift(out_length, 3); - if ((matches < (last_lit / 2)) && out_length < in_length / 2) - return true; - } - - return (last_lit == lit_bufsize - 1); - // We avoid equality with lit_bufsize because of wraparound at 64K - // on 16 bit machines and because stored blocks are restricted to - // 64K-1 bytes. - } - - // Send the block data compressed using the given Huffman trees - internal void compress_block(short[] ltree, short[] dtree) - { - int dist; // distance of matched string - int lc; // match length or unmatched char (if dist == 0) - int lx = 0; // running index in l_buf - int code; // the code to send - int extra; // number of extra bits to send - - if (last_lit != 0) - { - do - { - dist = ((pending_buf[d_buf + lx * 2] << 8) & 0xff00) | (pending_buf[d_buf + lx * 2 + 1] & 0xff); - lc = (pending_buf[l_buf + lx]) & 0xff; lx++; - - if (dist == 0) - { - send_code(lc, ltree); // send a literal byte - } - else - { - // Here, lc is the match length - MIN_MATCH - code = Tree._length_code[lc]; - - send_code(code + LITERALS + 1, ltree); // send the length code - extra = Tree.extra_lbits[code]; - if (extra != 0) - { - lc -= Tree.base_length[code]; - send_bits(lc, extra); // send the extra length bits - } - dist--; // dist is now the match distance - 1 - code = Tree.d_code(dist); - - send_code(code, dtree); // send the distance code - extra = Tree.extra_dbits[code]; - if (extra != 0) - { - dist -= Tree.base_dist[code]; - send_bits(dist, extra); // send the extra distance bits - } - } // literal or match pair ? - - // Check that the overlay between pending_buf and d_buf+l_buf is ok: - } - while (lx < last_lit); - } - - send_code(END_BLOCK, ltree); - last_eob_len = ltree[END_BLOCK * 2 + 1]; - } - - // Set the data type to ASCII or BINARY, using a crude approximation: - // binary if more than 20% of the bytes are <= 6 or >= 128, ascii otherwise. - // IN assertion: the fields freq of dyn_ltree are set and the total of all - // frequencies does not exceed 64K (to fit in an int on 16 bit machines). - internal void set_data_type() - { - int n = 0; - int ascii_freq = 0; - int bin_freq = 0; - while (n < 7) - { - bin_freq += dyn_ltree[n * 2]; n++; - } - while (n < 128) - { - ascii_freq += dyn_ltree[n * 2]; n++; - } - while (n < LITERALS) - { - bin_freq += dyn_ltree[n * 2]; n++; - } - data_type = (byte) (bin_freq > (SupportClass.URShift(ascii_freq, 2))?Z_BINARY:Z_ASCII); - } - - // Flush the bit buffer, keeping at most 7 bits in it. - internal void bi_flush() - { - if (bi_valid == 16) - { - put_short(bi_buf); - bi_buf = 0; - bi_valid = 0; - } - else if (bi_valid >= 8) - { - put_byte((byte) bi_buf); - bi_buf = (short) (SupportClass.URShift(bi_buf, 8)); - bi_valid -= 8; - } - } - - // Flush the bit buffer and align the output on a byte boundary - internal void bi_windup() - { - if (bi_valid > 8) - { - put_short(bi_buf); - } - else if (bi_valid > 0) - { - put_byte((byte) bi_buf); - } - bi_buf = 0; - bi_valid = 0; - } - - // Copy a stored block, storing first the length and its - // one's complement if requested. - internal void copy_block(int buf, int len, bool header) - { - - bi_windup(); // align on byte boundary - last_eob_len = 8; // enough lookahead for inflate - - if (header) - { - put_short((short) len); - put_short((short) ~ len); - } - - // while(len--!=0) { - // put_byte(window[buf+index]); - // index++; - // } - put_byte(window, buf, len); - } - - internal void flush_block_only(bool eof) - { - _tr_flush_block(block_start >= 0?block_start:- 1, strstart - block_start, eof); - block_start = strstart; - strm.flush_pending(); - } - - // Copy without compression as much as possible from the input stream, return - // the current block state. - // This function does not insert new strings in the dictionary since - // uncompressible data is probably not useful. This function is used - // only for the level=0 compression option. - // NOTE: this function should be optimized to avoid extra copying from - // window to pending_buf. - internal int deflate_stored(int flush) - { - // Stored blocks are limited to 0xffff bytes, pending_buf is limited - // to pending_buf_size, and each stored block has a 5 byte header: - - int max_block_size = 0xffff; - int max_start; - - if (max_block_size > pending_buf_size - 5) - { - max_block_size = pending_buf_size - 5; - } - - // Copy as much as possible from input to output: - while (true) - { - // Fill the window as much as possible: - if (lookahead <= 1) - { - fill_window(); - if (lookahead == 0 && flush == Z_NO_FLUSH) - return NeedMore; - if (lookahead == 0) - break; // flush the current block - } - - strstart += lookahead; - lookahead = 0; - - // Emit a stored block if pending_buf will be full: - max_start = block_start + max_block_size; - if (strstart == 0 || strstart >= max_start) - { - // strstart == 0 is possible when wraparound on 16-bit machine - lookahead = (int) (strstart - max_start); - strstart = (int) max_start; - - flush_block_only(false); - if (strm.avail_out == 0) - return NeedMore; - } - - // Flush if we may have to slide, otherwise block_start may become - // negative and the data will be gone: - if (strstart - block_start >= w_size - MIN_LOOKAHEAD) - { - flush_block_only(false); - if (strm.avail_out == 0) - return NeedMore; - } - } - - flush_block_only(flush == Z_FINISH); - if (strm.avail_out == 0) - return (flush == Z_FINISH)?FinishStarted:NeedMore; - - return flush == Z_FINISH?FinishDone:BlockDone; - } - - // Send a stored block - internal void _tr_stored_block(int buf, int stored_len, bool eof) - { - send_bits((STORED_BLOCK << 1) + (eof?1:0), 3); // send block type - copy_block(buf, stored_len, true); // with header - } - - // Determine the best encoding for the current block: dynamic trees, static - // trees or store, and output the encoded block to the zip file. - internal void _tr_flush_block(int buf, int stored_len, bool eof) - { - int opt_lenb, static_lenb; // opt_len and static_len in bytes - int max_blindex = 0; // index of last bit length code of non zero freq - - // Build the Huffman trees unless a stored block is forced - if (level > 0) - { - // Check if the file is ascii or binary - if (data_type == Z_UNKNOWN) - set_data_type(); - - // Construct the literal and distance trees - l_desc.build_tree(this); - - d_desc.build_tree(this); - - // At this point, opt_len and static_len are the total bit lengths of - // the compressed block data, excluding the tree representations. - - // Build the bit length tree for the above two trees, and get the index - // in bl_order of the last bit length code to send. - max_blindex = build_bl_tree(); - - // Determine the best encoding. Compute first the block length in bytes - opt_lenb = SupportClass.URShift((opt_len + 3 + 7), 3); - static_lenb = SupportClass.URShift((static_len + 3 + 7), 3); - - if (static_lenb <= opt_lenb) - opt_lenb = static_lenb; - } - else - { - opt_lenb = static_lenb = stored_len + 5; // force a stored block - } - - if (stored_len + 4 <= opt_lenb && buf != - 1) - { - // 4: two words for the lengths - // The test buf != NULL is only necessary if LIT_BUFSIZE > WSIZE. - // Otherwise we can't have processed more than WSIZE input bytes since - // the last block flush, because compression would have been - // successful. If LIT_BUFSIZE <= WSIZE, it is never too late to - // transform a block into a stored block. - _tr_stored_block(buf, stored_len, eof); - } - else if (static_lenb == opt_lenb) - { - send_bits((STATIC_TREES << 1) + (eof?1:0), 3); - compress_block(StaticTree.static_ltree, StaticTree.static_dtree); - } - else - { - send_bits((DYN_TREES << 1) + (eof?1:0), 3); - send_all_trees(l_desc.max_code + 1, d_desc.max_code + 1, max_blindex + 1); - compress_block(dyn_ltree, dyn_dtree); - } - - // The above check is made mod 2^32, for files larger than 512 MB - // and uLong implemented on 32 bits. - - init_block(); - - if (eof) - { - bi_windup(); - } - } - - // Fill the window when the lookahead becomes insufficient. - // Updates strstart and lookahead. - // - // IN assertion: lookahead < MIN_LOOKAHEAD - // OUT assertions: strstart <= window_size-MIN_LOOKAHEAD - // At least one byte has been read, or avail_in == 0; reads are - // performed for at least two bytes (required for the zip translate_eol - // option -- not supported here). - internal void fill_window() - { - int n, m; - int p; - int more; // Amount of free space at the end of the window. - - do - { - more = (window_size - lookahead - strstart); - - // Deal with !@#$% 64K limit: - if (more == 0 && strstart == 0 && lookahead == 0) - { - more = w_size; - } - else if (more == - 1) - { - // Very unlikely, but possible on 16 bit machine if strstart == 0 - // and lookahead == 1 (input done one byte at time) - more--; - - // If the window is almost full and there is insufficient lookahead, - // move the upper half to the lower one to make room in the upper half. - } - else if (strstart >= w_size + w_size - MIN_LOOKAHEAD) - { - Array.Copy(window, w_size, window, 0, w_size); - match_start -= w_size; - strstart -= w_size; // we now have strstart >= MAX_DIST - block_start -= w_size; - - // Slide the hash table (could be avoided with 32 bit values - // at the expense of memory usage). We slide even when level == 0 - // to keep the hash table consistent if we switch back to level > 0 - // later. (Using level 0 permanently is not an optimal usage of - // zlib, so we don't care about this pathological case.) - - n = hash_size; - p = n; - do - { - m = (head[--p] & 0xffff); - head[p] = (short)(m >= w_size?(m - w_size):0); - //head[p] = (m >= w_size?(short) (m - w_size):0); - } - while (--n != 0); - - n = w_size; - p = n; - do - { - m = (prev[--p] & 0xffff); - prev[p] = (short)(m >= w_size?(m - w_size):0); - //prev[p] = (m >= w_size?(short) (m - w_size):0); - // If n is not on any hash chain, prev[n] is garbage but - // its value will never be used. - } - while (--n != 0); - more += w_size; - } - - if (strm.avail_in == 0) - return ; - - // If there was no sliding: - // strstart <= WSIZE+MAX_DIST-1 && lookahead <= MIN_LOOKAHEAD - 1 && - // more == window_size - lookahead - strstart - // => more >= window_size - (MIN_LOOKAHEAD-1 + WSIZE + MAX_DIST-1) - // => more >= window_size - 2*WSIZE + 2 - // In the BIG_MEM or MMAP case (not yet supported), - // window_size == input_size + MIN_LOOKAHEAD && - // strstart + s->lookahead <= input_size => more >= MIN_LOOKAHEAD. - // Otherwise, window_size == 2*WSIZE so more >= 2. - // If there was sliding, more >= WSIZE. So in all cases, more >= 2. - - n = strm.read_buf(window, strstart + lookahead, more); - lookahead += n; - - // Initialize the hash value now that we have some input: - if (lookahead >= MIN_MATCH) - { - ins_h = window[strstart] & 0xff; - ins_h = (((ins_h) << hash_shift) ^ (window[strstart + 1] & 0xff)) & hash_mask; - } - // If the whole input has less than MIN_MATCH bytes, ins_h is garbage, - // but this is not important since only literal bytes will be emitted. - } - while (lookahead < MIN_LOOKAHEAD && strm.avail_in != 0); - } - - // Compress as much as possible from the input stream, return the current - // block state. - // This function does not perform lazy evaluation of matches and inserts - // new strings in the dictionary only for unmatched strings or for short - // matches. It is used only for the fast compression options. - internal int deflate_fast(int flush) - { - // short hash_head = 0; // head of the hash chain - int hash_head = 0; // head of the hash chain - bool bflush; // set if current block must be flushed - - while (true) - { - // Make sure that we always have enough lookahead, except - // at the end of the input file. We need MAX_MATCH bytes - // for the next match, plus MIN_MATCH bytes to insert the - // string following the next match. - if (lookahead < MIN_LOOKAHEAD) - { - fill_window(); - if (lookahead < MIN_LOOKAHEAD && flush == Z_NO_FLUSH) - { - return NeedMore; - } - if (lookahead == 0) - break; // flush the current block - } - - // Insert the string window[strstart .. strstart+2] in the - // dictionary, and set hash_head to the head of the hash chain: - if (lookahead >= MIN_MATCH) - { - ins_h = (((ins_h) << hash_shift) ^ (window[(strstart) + (MIN_MATCH - 1)] & 0xff)) & hash_mask; - - // prev[strstart&w_mask]=hash_head=head[ins_h]; - hash_head = (head[ins_h] & 0xffff); - prev[strstart & w_mask] = head[ins_h]; - head[ins_h] = (short) strstart; - } - - // Find the longest match, discarding those <= prev_length. - // At this point we have always match_length < MIN_MATCH - - if (hash_head != 0L && ((strstart - hash_head) & 0xffff) <= w_size - MIN_LOOKAHEAD) - { - // To simplify the code, we prevent matches with the string - // of window index 0 (in particular we have to avoid a match - // of the string with itself at the start of the input file). - if (strategy != Z_HUFFMAN_ONLY) - { - match_length = longest_match(hash_head); - } - // longest_match() sets match_start - } - if (match_length >= MIN_MATCH) - { - // check_match(strstart, match_start, match_length); - - bflush = _tr_tally(strstart - match_start, match_length - MIN_MATCH); - - lookahead -= match_length; - - // Insert new strings in the hash table only if the match length - // is not too large. This saves time but degrades compression. - if (match_length <= max_lazy_match && lookahead >= MIN_MATCH) - { - match_length--; // string at strstart already in hash table - do - { - strstart++; - - ins_h = ((ins_h << hash_shift) ^ (window[(strstart) + (MIN_MATCH - 1)] & 0xff)) & hash_mask; - // prev[strstart&w_mask]=hash_head=head[ins_h]; - hash_head = (head[ins_h] & 0xffff); - prev[strstart & w_mask] = head[ins_h]; - head[ins_h] = (short) strstart; - - // strstart never exceeds WSIZE-MAX_MATCH, so there are - // always MIN_MATCH bytes ahead. - } - while (--match_length != 0); - strstart++; - } - else - { - strstart += match_length; - match_length = 0; - ins_h = window[strstart] & 0xff; - - ins_h = (((ins_h) << hash_shift) ^ (window[strstart + 1] & 0xff)) & hash_mask; - // If lookahead < MIN_MATCH, ins_h is garbage, but it does not - // matter since it will be recomputed at next deflate call. - } - } - else - { - // No match, output a literal byte - - bflush = _tr_tally(0, window[strstart] & 0xff); - lookahead--; - strstart++; - } - if (bflush) - { - - flush_block_only(false); - if (strm.avail_out == 0) - return NeedMore; - } - } - - flush_block_only(flush == Z_FINISH); - if (strm.avail_out == 0) - { - if (flush == Z_FINISH) - return FinishStarted; - else - return NeedMore; - } - return flush == Z_FINISH?FinishDone:BlockDone; - } - - // Same as above, but achieves better compression. We use a lazy - // evaluation for matches: a match is finally adopted only if there is - // no better match at the next window position. - internal int deflate_slow(int flush) - { - // short hash_head = 0; // head of hash chain - int hash_head = 0; // head of hash chain - bool bflush; // set if current block must be flushed - - // Process the input block. - while (true) - { - // Make sure that we always have enough lookahead, except - // at the end of the input file. We need MAX_MATCH bytes - // for the next match, plus MIN_MATCH bytes to insert the - // string following the next match. - - if (lookahead < MIN_LOOKAHEAD) - { - fill_window(); - if (lookahead < MIN_LOOKAHEAD && flush == Z_NO_FLUSH) - { - return NeedMore; - } - if (lookahead == 0) - break; // flush the current block - } - - // Insert the string window[strstart .. strstart+2] in the - // dictionary, and set hash_head to the head of the hash chain: - - if (lookahead >= MIN_MATCH) - { - ins_h = (((ins_h) << hash_shift) ^ (window[(strstart) + (MIN_MATCH - 1)] & 0xff)) & hash_mask; - // prev[strstart&w_mask]=hash_head=head[ins_h]; - hash_head = (head[ins_h] & 0xffff); - prev[strstart & w_mask] = head[ins_h]; - head[ins_h] = (short) strstart; - } - - // Find the longest match, discarding those <= prev_length. - prev_length = match_length; prev_match = match_start; - match_length = MIN_MATCH - 1; - - if (hash_head != 0 && prev_length < max_lazy_match && ((strstart - hash_head) & 0xffff) <= w_size - MIN_LOOKAHEAD) - { - // To simplify the code, we prevent matches with the string - // of window index 0 (in particular we have to avoid a match - // of the string with itself at the start of the input file). - - if (strategy != Z_HUFFMAN_ONLY) - { - match_length = longest_match(hash_head); - } - // longest_match() sets match_start - - if (match_length <= 5 && (strategy == Z_FILTERED || (match_length == MIN_MATCH && strstart - match_start > 4096))) - { - - // If prev_match is also MIN_MATCH, match_start is garbage - // but we will ignore the current match anyway. - match_length = MIN_MATCH - 1; - } - } - - // If there was a match at the previous step and the current - // match is not better, output the previous match: - if (prev_length >= MIN_MATCH && match_length <= prev_length) - { - int max_insert = strstart + lookahead - MIN_MATCH; - // Do not insert strings in hash table beyond this. - - // check_match(strstart-1, prev_match, prev_length); - - bflush = _tr_tally(strstart - 1 - prev_match, prev_length - MIN_MATCH); - - // Insert in hash table all strings up to the end of the match. - // strstart-1 and strstart are already inserted. If there is not - // enough lookahead, the last two strings are not inserted in - // the hash table. - lookahead -= (prev_length - 1); - prev_length -= 2; - do - { - if (++strstart <= max_insert) - { - ins_h = (((ins_h) << hash_shift) ^ (window[(strstart) + (MIN_MATCH - 1)] & 0xff)) & hash_mask; - //prev[strstart&w_mask]=hash_head=head[ins_h]; - hash_head = (head[ins_h] & 0xffff); - prev[strstart & w_mask] = head[ins_h]; - head[ins_h] = (short) strstart; - } - } - while (--prev_length != 0); - match_available = 0; - match_length = MIN_MATCH - 1; - strstart++; - - if (bflush) - { - flush_block_only(false); - if (strm.avail_out == 0) - return NeedMore; - } - } - else if (match_available != 0) - { - - // If there was no match at the previous position, output a - // single literal. If there was a match but the current match - // is longer, truncate the previous match to a single literal. - - bflush = _tr_tally(0, window[strstart - 1] & 0xff); - - if (bflush) - { - flush_block_only(false); - } - strstart++; - lookahead--; - if (strm.avail_out == 0) - return NeedMore; - } - else - { - // There is no previous match to compare with, wait for - // the next step to decide. - - match_available = 1; - strstart++; - lookahead--; - } - } - - if (match_available != 0) - { - bflush = _tr_tally(0, window[strstart - 1] & 0xff); - match_available = 0; - } - flush_block_only(flush == Z_FINISH); - - if (strm.avail_out == 0) - { - if (flush == Z_FINISH) - return FinishStarted; - else - return NeedMore; - } - - return flush == Z_FINISH?FinishDone:BlockDone; - } - - internal int longest_match(int cur_match) - { - int chain_length = max_chain_length; // max hash chain length - int scan = strstart; // current string - int match; // matched string - int len; // length of current match - int best_len = prev_length; // best match length so far - int limit = strstart > (w_size - MIN_LOOKAHEAD)?strstart - (w_size - MIN_LOOKAHEAD):0; - int nice_match = this.nice_match; - - // Stop when cur_match becomes <= limit. To simplify the code, - // we prevent matches with the string of window index 0. - - int wmask = w_mask; - - int strend = strstart + MAX_MATCH; - byte scan_end1 = window[scan + best_len - 1]; - byte scan_end = window[scan + best_len]; - - // The code is optimized for HASH_BITS >= 8 and MAX_MATCH-2 multiple of 16. - // It is easy to get rid of this optimization if necessary. - - // Do not waste too much time if we already have a good match: - if (prev_length >= good_match) - { - chain_length >>= 2; - } - - // Do not look for matches beyond the end of the input. This is necessary - // to make deflate deterministic. - if (nice_match > lookahead) - nice_match = lookahead; - - do - { - match = cur_match; - - // Skip to next match if the match length cannot increase - // or if the match length is less than 2: - if (window[match + best_len] != scan_end || window[match + best_len - 1] != scan_end1 || window[match] != window[scan] || window[++match] != window[scan + 1]) - continue; - - // The check at best_len-1 can be removed because it will be made - // again later. (This heuristic is not always a win.) - // It is not necessary to compare scan[2] and match[2] since they - // are always equal when the other bytes match, given that - // the hash keys are equal and that HASH_BITS >= 8. - scan += 2; match++; - - // We check for insufficient lookahead only every 8th comparison; - // the 256th check will be made at strstart+258. - do - { - } - while (window[++scan] == window[++match] && window[++scan] == window[++match] && window[++scan] == window[++match] && window[++scan] == window[++match] && window[++scan] == window[++match] && window[++scan] == window[++match] && window[++scan] == window[++match] && window[++scan] == window[++match] && scan < strend); - - len = MAX_MATCH - (int) (strend - scan); - scan = strend - MAX_MATCH; - - if (len > best_len) - { - match_start = cur_match; - best_len = len; - if (len >= nice_match) - break; - scan_end1 = window[scan + best_len - 1]; - scan_end = window[scan + best_len]; - } - } - while ((cur_match = (prev[cur_match & wmask] & 0xffff)) > limit && --chain_length != 0); - - if (best_len <= lookahead) - return best_len; - return lookahead; - } - - internal int deflateInit(ZStream strm, int level, int bits) - { - return deflateInit2(strm, level, Z_DEFLATED, bits, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY); - } - internal int deflateInit(ZStream strm, int level) - { - return deflateInit(strm, level, MAX_WBITS); - } - internal int deflateInit2(ZStream strm, int level, int method, int windowBits, int memLevel, int strategy) - { - int noheader = 0; - // byte[] my_version=ZLIB_VERSION; - - // - // if (version == null || version[0] != my_version[0] - // || stream_size != sizeof(z_stream)) { - // return Z_VERSION_ERROR; - // } - - strm.msg = null; - - if (level == Z_DEFAULT_COMPRESSION) - level = 6; - - if (windowBits < 0) - { - // undocumented feature: suppress zlib header - noheader = 1; - windowBits = - windowBits; - } - - if (memLevel < 1 || memLevel > MAX_MEM_LEVEL || method != Z_DEFLATED || windowBits < 9 || windowBits > 15 || level < 0 || level > 9 || strategy < 0 || strategy > Z_HUFFMAN_ONLY) - { - return Z_STREAM_ERROR; - } - - strm.dstate = (Deflate) this; - - this.noheader = noheader; - w_bits = windowBits; - w_size = 1 << w_bits; - w_mask = w_size - 1; - - hash_bits = memLevel + 7; - hash_size = 1 << hash_bits; - hash_mask = hash_size - 1; - hash_shift = ((hash_bits + MIN_MATCH - 1) / MIN_MATCH); - - window = new byte[w_size * 2]; - prev = new short[w_size]; - head = new short[hash_size]; - - lit_bufsize = 1 << (memLevel + 6); // 16K elements by default - - // We overlay pending_buf and d_buf+l_buf. This works since the average - // output size for (length,distance) codes is <= 24 bits. - pending_buf = new byte[lit_bufsize * 4]; - pending_buf_size = lit_bufsize * 4; - - d_buf = lit_bufsize; - l_buf = (1 + 2) * lit_bufsize; - - this.level = level; - - //System.out.println("level="+level); - - this.strategy = strategy; - this.method = (byte) method; - - return deflateReset(strm); - } - - internal int deflateReset(ZStream strm) - { - strm.total_in = strm.total_out = 0; - strm.msg = null; // - strm.data_type = Z_UNKNOWN; - - pending = 0; - pending_out = 0; - - if (noheader < 0) - { - noheader = 0; // was set to -1 by deflate(..., Z_FINISH); - } - status = (noheader != 0)?BUSY_STATE:INIT_STATE; - strm.adler = strm._adler.adler32(0, null, 0, 0); - - last_flush = Z_NO_FLUSH; - - tr_init(); - lm_init(); - return Z_OK; - } - - internal int deflateEnd() - { - if (status != INIT_STATE && status != BUSY_STATE && status != FINISH_STATE) - { - return Z_STREAM_ERROR; - } - // Deallocate in reverse order of allocations: - pending_buf = null; - head = null; - prev = null; - window = null; - // free - // dstate=null; - return status == BUSY_STATE?Z_DATA_ERROR:Z_OK; - } - - internal int deflateParams(ZStream strm, int _level, int _strategy) - { - int err = Z_OK; - - if (_level == Z_DEFAULT_COMPRESSION) - { - _level = 6; - } - if (_level < 0 || _level > 9 || _strategy < 0 || _strategy > Z_HUFFMAN_ONLY) - { - return Z_STREAM_ERROR; - } - - if (config_table[level].func != config_table[_level].func && strm.total_in != 0) - { - // Flush the last buffer: - err = strm.deflate(Z_PARTIAL_FLUSH); - } - - if (level != _level) - { - level = _level; - max_lazy_match = config_table[level].max_lazy; - good_match = config_table[level].good_length; - nice_match = config_table[level].nice_length; - max_chain_length = config_table[level].max_chain; - } - strategy = _strategy; - return err; - } - - internal int deflateSetDictionary(ZStream strm, byte[] dictionary, int dictLength) - { - int length = dictLength; - int index = 0; - - if (dictionary == null || status != INIT_STATE) - return Z_STREAM_ERROR; - - strm.adler = strm._adler.adler32(strm.adler, dictionary, 0, dictLength); - - if (length < MIN_MATCH) - return Z_OK; - if (length > w_size - MIN_LOOKAHEAD) - { - length = w_size - MIN_LOOKAHEAD; - index = dictLength - length; // use the tail of the dictionary - } - Array.Copy(dictionary, index, window, 0, length); - strstart = length; - block_start = length; - - // Insert all strings in the hash table (except for the last two bytes). - // s->lookahead stays null, so s->ins_h will be recomputed at the next - // call of fill_window. - - ins_h = window[0] & 0xff; - ins_h = (((ins_h) << hash_shift) ^ (window[1] & 0xff)) & hash_mask; - - for (int n = 0; n <= length - MIN_MATCH; n++) - { - ins_h = (((ins_h) << hash_shift) ^ (window[(n) + (MIN_MATCH - 1)] & 0xff)) & hash_mask; - prev[n & w_mask] = head[ins_h]; - head[ins_h] = (short) n; - } - return Z_OK; - } - - internal int deflate(ZStream strm, int flush) - { - int old_flush; - - if (flush > Z_FINISH || flush < 0) - { - return Z_STREAM_ERROR; - } - - if (strm.next_out == null || (strm.next_in == null && strm.avail_in != 0) || (status == FINISH_STATE && flush != Z_FINISH)) - { - strm.msg = z_errmsg[Z_NEED_DICT - (Z_STREAM_ERROR)]; - return Z_STREAM_ERROR; - } - if (strm.avail_out == 0) - { - strm.msg = z_errmsg[Z_NEED_DICT - (Z_BUF_ERROR)]; - return Z_BUF_ERROR; - } - - this.strm = strm; // just in case - old_flush = last_flush; - last_flush = flush; - - // Write the zlib header - if (status == INIT_STATE) - { - int header = (Z_DEFLATED + ((w_bits - 8) << 4)) << 8; - int level_flags = ((level - 1) & 0xff) >> 1; - - if (level_flags > 3) - level_flags = 3; - header |= (level_flags << 6); - if (strstart != 0) - header |= PRESET_DICT; - header += 31 - (header % 31); - - status = BUSY_STATE; - putShortMSB(header); - - - // Save the adler32 of the preset dictionary: - if (strstart != 0) - { - putShortMSB((int) (SupportClass.URShift(strm.adler, 16))); - putShortMSB((int) (strm.adler & 0xffff)); - } - strm.adler = strm._adler.adler32(0, null, 0, 0); - } - - // Flush as much pending output as possible - if (pending != 0) - { - strm.flush_pending(); - if (strm.avail_out == 0) - { - //System.out.println(" avail_out==0"); - // Since avail_out is 0, deflate will be called again with - // more output space, but possibly with both pending and - // avail_in equal to zero. There won't be anything to do, - // but this is not an error situation so make sure we - // return OK instead of BUF_ERROR at next call of deflate: - last_flush = - 1; - return Z_OK; - } - - // Make sure there is something to do and avoid duplicate consecutive - // flushes. For repeated and useless calls with Z_FINISH, we keep - // returning Z_STREAM_END instead of Z_BUFF_ERROR. - } - else if (strm.avail_in == 0 && flush <= old_flush && flush != Z_FINISH) - { - strm.msg = z_errmsg[Z_NEED_DICT - (Z_BUF_ERROR)]; - return Z_BUF_ERROR; - } - - // User must not provide more input after the first FINISH: - if (status == FINISH_STATE && strm.avail_in != 0) - { - strm.msg = z_errmsg[Z_NEED_DICT - (Z_BUF_ERROR)]; - return Z_BUF_ERROR; - } - - // Start a new block or continue the current one. - if (strm.avail_in != 0 || lookahead != 0 || (flush != Z_NO_FLUSH && status != FINISH_STATE)) - { - int bstate = - 1; - switch (config_table[level].func) - { - - case STORED: - bstate = deflate_stored(flush); - break; - - case FAST: - bstate = deflate_fast(flush); - break; - - case SLOW: - bstate = deflate_slow(flush); - break; - - default: - break; - - } - - if (bstate == FinishStarted || bstate == FinishDone) - { - status = FINISH_STATE; - } - if (bstate == NeedMore || bstate == FinishStarted) - { - if (strm.avail_out == 0) - { - last_flush = - 1; // avoid BUF_ERROR next call, see above - } - return Z_OK; - // If flush != Z_NO_FLUSH && avail_out == 0, the next call - // of deflate should use the same flush parameter to make sure - // that the flush is complete. So we don't have to output an - // empty block here, this will be done at next call. This also - // ensures that for a very small output buffer, we emit at most - // one empty block. - } - - if (bstate == BlockDone) - { - if (flush == Z_PARTIAL_FLUSH) - { - _tr_align(); - } - else - { - // FULL_FLUSH or SYNC_FLUSH - _tr_stored_block(0, 0, false); - // For a full flush, this empty block will be recognized - // as a special marker by inflate_sync(). - if (flush == Z_FULL_FLUSH) - { - //state.head[s.hash_size-1]=0; - for (int i = 0; i < hash_size; i++) - // forget history - head[i] = 0; - } - } - strm.flush_pending(); - if (strm.avail_out == 0) - { - last_flush = - 1; // avoid BUF_ERROR at next call, see above - return Z_OK; - } - } - } - - if (flush != Z_FINISH) - return Z_OK; - if (noheader != 0) - return Z_STREAM_END; - - // Write the zlib trailer (adler32) - putShortMSB((int) (SupportClass.URShift(strm.adler, 16))); - putShortMSB((int) (strm.adler & 0xffff)); - strm.flush_pending(); - - // If avail_out is zero, the application will call deflate again - // to flush the rest. - noheader = - 1; // write the trailer only once! - return pending != 0?Z_OK:Z_STREAM_END; - } - static Deflate() - { - { - config_table = new Config[10]; - // good lazy nice chain - config_table[0] = new Config(0, 0, 0, 0, STORED); - config_table[1] = new Config(4, 4, 8, 4, FAST); - config_table[2] = new Config(4, 5, 16, 8, FAST); - config_table[3] = new Config(4, 6, 32, 32, FAST); - - config_table[4] = new Config(4, 4, 16, 16, SLOW); - config_table[5] = new Config(8, 16, 32, 32, SLOW); - config_table[6] = new Config(8, 16, 128, 128, SLOW); - config_table[7] = new Config(8, 32, 128, 256, SLOW); - config_table[8] = new Config(32, 128, 258, 1024, SLOW); - config_table[9] = new Config(32, 258, 258, 4096, SLOW); - } - } - } -} \ No newline at end of file diff --git a/src/Zlib.net/InfBlocks.cs b/src/Zlib.net/InfBlocks.cs deleted file mode 100644 index 9d28592..0000000 --- a/src/Zlib.net/InfBlocks.cs +++ /dev/null @@ -1,720 +0,0 @@ -// Copyright (c) 2006, ComponentAce -// http://www.componentace.com -// All rights reserved. - -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -// Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -// Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -// Neither the name of ComponentAce nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -/* -Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -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. The names of the authors may not be used to endorse or promote products -derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 JCRAFT, -INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -/* -* This program is based on zlib-1.1.3, so all credit should go authors -* Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) -* and contributors of zlib. -*/ -using System; -namespace ComponentAce.Compression.Libs.zlib -{ - - sealed class InfBlocks - { - private const int MANY = 1440; - - // And'ing with mask[n] masks the lower n bits - private static readonly int[] inflate_mask = new int[]{0x00000000, 0x00000001, 0x00000003, 0x00000007, 0x0000000f, 0x0000001f, 0x0000003f, 0x0000007f, 0x000000ff, 0x000001ff, 0x000003ff, 0x000007ff, 0x00000fff, 0x00001fff, 0x00003fff, 0x00007fff, 0x0000ffff}; - - // Table for deflate from PKZIP's appnote.txt. - internal static readonly int[] border = new int[]{16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}; - - private const int Z_OK = 0; - private const int Z_STREAM_END = 1; - private const int Z_NEED_DICT = 2; - private const int Z_ERRNO = - 1; - private const int Z_STREAM_ERROR = - 2; - private const int Z_DATA_ERROR = - 3; - private const int Z_MEM_ERROR = - 4; - private const int Z_BUF_ERROR = - 5; - private const int Z_VERSION_ERROR = - 6; - - private const int TYPE = 0; // get type bits (3, including end bit) - private const int LENS = 1; // get lengths for stored - private const int STORED = 2; // processing stored block - private const int TABLE = 3; // get table lengths - private const int BTREE = 4; // get bit lengths tree for a dynamic block - private const int DTREE = 5; // get length, distance trees for a dynamic block - private const int CODES = 6; // processing fixed or dynamic block - private const int DRY = 7; // output remaining window bytes - private const int DONE = 8; // finished last block, done - private const int BAD = 9; // ot a data error--stuck here - - internal int mode; // current inflate_block mode - - internal int left; // if STORED, bytes left to copy - - internal int table; // table lengths (14 bits) - internal int index; // index into blens (or border) - internal int[] blens; // bit lengths of codes - internal int[] bb = new int[1]; // bit length tree depth - internal int[] tb = new int[1]; // bit length decoding tree - - internal InfCodes codes; // if CODES, current state - - internal int last; // true if this block is the last block - - // mode independent information - internal int bitk; // bits in bit buffer - internal int bitb; // bit buffer - internal int[] hufts; // single malloc for tree space - internal byte[] window; // sliding window - internal int end; // one byte after sliding window - internal int read; // window read pointer - internal int write; // window write pointer - internal System.Object checkfn; // check function - internal long check; // check on output - - internal InfBlocks(ZStream z, System.Object checkfn, int w) - { - hufts = new int[MANY * 3]; - window = new byte[w]; - end = w; - this.checkfn = checkfn; - mode = TYPE; - reset(z, null); - } - - internal void reset(ZStream z, long[] c) - { - if (c != null) - c[0] = check; - if (mode == BTREE || mode == DTREE) - { - blens = null; - } - if (mode == CODES) - { - codes.free(z); - } - mode = TYPE; - bitk = 0; - bitb = 0; - read = write = 0; - - if (checkfn != null) - z.adler = check = z._adler.adler32(0L, null, 0, 0); - } - - internal int proc(ZStream z, int r) - { - int t; // temporary storage - int b; // bit buffer - int k; // bits in bit buffer - int p; // input data pointer - int n; // bytes available there - int q; // output window write pointer - int m; // bytes to end of window or read pointer - - // copy input/output information to locals (UPDATE macro restores) - { - p = z.next_in_index; n = z.avail_in; b = bitb; k = bitk; - } - { - q = write; m = (int) (q < read?read - q - 1:end - q); - } - - // process input based on current state - while (true) - { - switch (mode) - { - - case TYPE: - - while (k < (3)) - { - if (n != 0) - { - r = Z_OK; - } - else - { - bitb = b; bitk = k; - z.avail_in = n; - z.total_in += p - z.next_in_index; z.next_in_index = p; - write = q; - return inflate_flush(z, r); - } - ; - n--; - b |= (z.next_in[p++] & 0xff) << k; - k += 8; - } - t = (int) (b & 7); - last = t & 1; - - switch (SupportClass.URShift(t, 1)) - { - - case 0: // stored - { - b = SupportClass.URShift(b, (3)); k -= (3); - } - t = k & 7; // go to byte boundary - - { - b = SupportClass.URShift(b, (t)); k -= (t); - } - mode = LENS; // get length of stored block - break; - - case 1: // fixed - { - int[] bl = new int[1]; - int[] bd = new int[1]; - int[][] tl = new int[1][]; - int[][] td = new int[1][]; - - InfTree.inflate_trees_fixed(bl, bd, tl, td, z); - codes = new InfCodes(bl[0], bd[0], tl[0], td[0], z); - } - - { - b = SupportClass.URShift(b, (3)); k -= (3); - } - - mode = CODES; - break; - - case 2: // dynamic - - { - b = SupportClass.URShift(b, (3)); k -= (3); - } - - mode = TABLE; - break; - - case 3: // illegal - - { - b = SupportClass.URShift(b, (3)); k -= (3); - } - mode = BAD; - z.msg = "invalid block type"; - r = Z_DATA_ERROR; - - bitb = b; bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - write = q; - return inflate_flush(z, r); - } - break; - - case LENS: - - while (k < (32)) - { - if (n != 0) - { - r = Z_OK; - } - else - { - bitb = b; bitk = k; - z.avail_in = n; - z.total_in += p - z.next_in_index; z.next_in_index = p; - write = q; - return inflate_flush(z, r); - } - ; - n--; - b |= (z.next_in[p++] & 0xff) << k; - k += 8; - } - - if (((SupportClass.URShift((~ b), 16)) & 0xffff) != (b & 0xffff)) - { - mode = BAD; - z.msg = "invalid stored block lengths"; - r = Z_DATA_ERROR; - - bitb = b; bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - write = q; - return inflate_flush(z, r); - } - left = (b & 0xffff); - b = k = 0; // dump bits - mode = left != 0?STORED:(last != 0?DRY:TYPE); - break; - - case STORED: - if (n == 0) - { - bitb = b; bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - write = q; - return inflate_flush(z, r); - } - - if (m == 0) - { - if (q == end && read != 0) - { - q = 0; m = (int) (q < read?read - q - 1:end - q); - } - if (m == 0) - { - write = q; - r = inflate_flush(z, r); - q = write; m = (int) (q < read?read - q - 1:end - q); - if (q == end && read != 0) - { - q = 0; m = (int) (q < read?read - q - 1:end - q); - } - if (m == 0) - { - bitb = b; bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - write = q; - return inflate_flush(z, r); - } - } - } - r = Z_OK; - - t = left; - if (t > n) - t = n; - if (t > m) - t = m; - Array.Copy(z.next_in, p, window, q, t); - p += t; n -= t; - q += t; m -= t; - if ((left -= t) != 0) - break; - mode = last != 0?DRY:TYPE; - break; - - case TABLE: - - while (k < (14)) - { - if (n != 0) - { - r = Z_OK; - } - else - { - bitb = b; bitk = k; - z.avail_in = n; - z.total_in += p - z.next_in_index; z.next_in_index = p; - write = q; - return inflate_flush(z, r); - } - ; - n--; - b |= (z.next_in[p++] & 0xff) << k; - k += 8; - } - - table = t = (b & 0x3fff); - if ((t & 0x1f) > 29 || ((t >> 5) & 0x1f) > 29) - { - mode = BAD; - z.msg = "too many length or distance symbols"; - r = Z_DATA_ERROR; - - bitb = b; bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - write = q; - return inflate_flush(z, r); - } - t = 258 + (t & 0x1f) + ((t >> 5) & 0x1f); - blens = new int[t]; - - { - b = SupportClass.URShift(b, (14)); k -= (14); - } - - index = 0; - mode = BTREE; - goto case BTREE; - - case BTREE: - while (index < 4 + (SupportClass.URShift(table, 10))) - { - while (k < (3)) - { - if (n != 0) - { - r = Z_OK; - } - else - { - bitb = b; bitk = k; - z.avail_in = n; - z.total_in += p - z.next_in_index; z.next_in_index = p; - write = q; - return inflate_flush(z, r); - } - ; - n--; - b |= (z.next_in[p++] & 0xff) << k; - k += 8; - } - - blens[border[index++]] = b & 7; - - { - b = SupportClass.URShift(b, (3)); k -= (3); - } - } - - while (index < 19) - { - blens[border[index++]] = 0; - } - - bb[0] = 7; - t = InfTree.inflate_trees_bits(blens, bb, tb, hufts, z); - if (t != Z_OK) - { - r = t; - if (r == Z_DATA_ERROR) - { - blens = null; - mode = BAD; - } - - bitb = b; bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - write = q; - return inflate_flush(z, r); - } - - index = 0; - mode = DTREE; - goto case DTREE; - - case DTREE: - while (true) - { - t = table; - if (!(index < 258 + (t & 0x1f) + ((t >> 5) & 0x1f))) - { - break; - } - - - int i, j, c; - - t = bb[0]; - - while (k < (t)) - { - if (n != 0) - { - r = Z_OK; - } - else - { - bitb = b; bitk = k; - z.avail_in = n; - z.total_in += p - z.next_in_index; z.next_in_index = p; - write = q; - return inflate_flush(z, r); - } - ; - n--; - b |= (z.next_in[p++] & 0xff) << k; - k += 8; - } - - if (tb[0] == - 1) - { - //System.err.println("null..."); - } - - t = hufts[(tb[0] + (b & inflate_mask[t])) * 3 + 1]; - c = hufts[(tb[0] + (b & inflate_mask[t])) * 3 + 2]; - - if (c < 16) - { - b = SupportClass.URShift(b, (t)); k -= (t); - blens[index++] = c; - } - else - { - // c == 16..18 - i = c == 18?7:c - 14; - j = c == 18?11:3; - - while (k < (t + i)) - { - if (n != 0) - { - r = Z_OK; - } - else - { - bitb = b; bitk = k; - z.avail_in = n; - z.total_in += p - z.next_in_index; z.next_in_index = p; - write = q; - return inflate_flush(z, r); - } - ; - n--; - b |= (z.next_in[p++] & 0xff) << k; - k += 8; - } - - b = SupportClass.URShift(b, (t)); k -= (t); - - j += (b & inflate_mask[i]); - - b = SupportClass.URShift(b, (i)); k -= (i); - - i = index; - t = table; - if (i + j > 258 + (t & 0x1f) + ((t >> 5) & 0x1f) || (c == 16 && i < 1)) - { - blens = null; - mode = BAD; - z.msg = "invalid bit length repeat"; - r = Z_DATA_ERROR; - - bitb = b; bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - write = q; - return inflate_flush(z, r); - } - - c = c == 16?blens[i - 1]:0; - do - { - blens[i++] = c; - } - while (--j != 0); - index = i; - } - } - - tb[0] = - 1; - { - int[] bl = new int[1]; - int[] bd = new int[1]; - int[] tl = new int[1]; - int[] td = new int[1]; - - - bl[0] = 9; // must be <= 9 for lookahead assumptions - bd[0] = 6; // must be <= 9 for lookahead assumptions - t = table; - t = InfTree.inflate_trees_dynamic(257 + (t & 0x1f), 1 + ((t >> 5) & 0x1f), blens, bl, bd, tl, td, hufts, z); - if (t != Z_OK) - { - if (t == Z_DATA_ERROR) - { - blens = null; - mode = BAD; - } - r = t; - - bitb = b; bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - write = q; - return inflate_flush(z, r); - } - - codes = new InfCodes(bl[0], bd[0], hufts, tl[0], hufts, td[0], z); - } - blens = null; - mode = CODES; - goto case CODES; - - case CODES: - bitb = b; bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - write = q; - - if ((r = codes.proc(this, z, r)) != Z_STREAM_END) - { - return inflate_flush(z, r); - } - r = Z_OK; - codes.free(z); - - p = z.next_in_index; n = z.avail_in; b = bitb; k = bitk; - q = write; m = (int) (q < read?read - q - 1:end - q); - - if (last == 0) - { - mode = TYPE; - break; - } - mode = DRY; - goto case DRY; - - case DRY: - write = q; - r = inflate_flush(z, r); - q = write; m = (int) (q < read?read - q - 1:end - q); - if (read != write) - { - bitb = b; bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - write = q; - return inflate_flush(z, r); - } - mode = DONE; - goto case DONE; - - case DONE: - r = Z_STREAM_END; - - bitb = b; bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - write = q; - return inflate_flush(z, r); - - case BAD: - r = Z_DATA_ERROR; - - bitb = b; bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - write = q; - return inflate_flush(z, r); - - - default: - r = Z_STREAM_ERROR; - - bitb = b; bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - write = q; - return inflate_flush(z, r); - - } - } - } - - internal void free(ZStream z) - { - reset(z, null); - window = null; - hufts = null; - //ZFREE(z, s); - } - - internal void set_dictionary(byte[] d, int start, int n) - { - Array.Copy(d, start, window, 0, n); - read = write = n; - } - - // Returns true if inflate is currently at the end of a block generated - // by Z_SYNC_FLUSH or Z_FULL_FLUSH. - internal int sync_point() - { - return mode == LENS?1:0; - } - - // copy as much as possible from the sliding window to the output area - internal int inflate_flush(ZStream z, int r) - { - int n; - int p; - int q; - - // local copies of source and destination pointers - p = z.next_out_index; - q = read; - - // compute number of bytes to copy as far as end of window - n = (int) ((q <= write?write:end) - q); - if (n > z.avail_out) - n = z.avail_out; - if (n != 0 && r == Z_BUF_ERROR) - r = Z_OK; - - // update counters - z.avail_out -= n; - z.total_out += n; - - // update check information - if (checkfn != null) - z.adler = check = z._adler.adler32(check, window, q, n); - - // copy as far as end of window - Array.Copy(window, q, z.next_out, p, n); - p += n; - q += n; - - // see if more to copy at beginning of window - if (q == end) - { - // wrap pointers - q = 0; - if (write == end) - write = 0; - - // compute bytes to copy - n = write - q; - if (n > z.avail_out) - n = z.avail_out; - if (n != 0 && r == Z_BUF_ERROR) - r = Z_OK; - - // update counters - z.avail_out -= n; - z.total_out += n; - - // update check information - if (checkfn != null) - z.adler = check = z._adler.adler32(check, window, q, n); - - // copy - Array.Copy(window, q, z.next_out, p, n); - p += n; - q += n; - } - - // update pointers - z.next_out_index = p; - read = q; - - // done - return r; - } - } -} \ No newline at end of file diff --git a/src/Zlib.net/InfCodes.cs b/src/Zlib.net/InfCodes.cs deleted file mode 100644 index ee279a1..0000000 --- a/src/Zlib.net/InfCodes.cs +++ /dev/null @@ -1,716 +0,0 @@ -// Copyright (c) 2006, ComponentAce -// http://www.componentace.com -// All rights reserved. - -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -// Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -// Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -// Neither the name of ComponentAce nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -/* -Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -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. The names of the authors may not be used to endorse or promote products -derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 JCRAFT, -INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -/* -* This program is based on zlib-1.1.3, so all credit should go authors -* Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) -* and contributors of zlib. -*/ -using System; -namespace ComponentAce.Compression.Libs.zlib -{ - - sealed class InfCodes - { - - private static readonly int[] inflate_mask = new int[]{0x00000000, 0x00000001, 0x00000003, 0x00000007, 0x0000000f, 0x0000001f, 0x0000003f, 0x0000007f, 0x000000ff, 0x000001ff, 0x000003ff, 0x000007ff, 0x00000fff, 0x00001fff, 0x00003fff, 0x00007fff, 0x0000ffff}; - - private const int Z_OK = 0; - private const int Z_STREAM_END = 1; - private const int Z_NEED_DICT = 2; - private const int Z_ERRNO = - 1; - private const int Z_STREAM_ERROR = - 2; - private const int Z_DATA_ERROR = - 3; - private const int Z_MEM_ERROR = - 4; - private const int Z_BUF_ERROR = - 5; - private const int Z_VERSION_ERROR = - 6; - - // waiting for "i:"=input, - // "o:"=output, - // "x:"=nothing - private const int START = 0; // x: set up for LEN - private const int LEN = 1; // i: get length/literal/eob next - private const int LENEXT = 2; // i: getting length extra (have base) - private const int DIST = 3; // i: get distance next - private const int DISTEXT = 4; // i: getting distance extra - private const int COPY = 5; // o: copying bytes in window, waiting for space - private const int LIT = 6; // o: got literal, waiting for output space - private const int WASH = 7; // o: got eob, possibly still output waiting - private const int END = 8; // x: got eob and all data flushed - private const int BADCODE = 9; // x: got error - - internal int mode; // current inflate_codes mode - - // mode dependent information - internal int len; - - internal int[] tree; // pointer into tree - internal int tree_index = 0; - internal int need; // bits needed - - internal int lit; - - // if EXT or COPY, where and how much - internal int get_Renamed; // bits to get for extra - internal int dist; // distance back to copy from - - internal byte lbits; // ltree bits decoded per branch - internal byte dbits; // dtree bits decoder per branch - internal int[] ltree; // literal/length/eob tree - internal int ltree_index; // literal/length/eob tree - internal int[] dtree; // distance tree - internal int dtree_index; // distance tree - - internal InfCodes(int bl, int bd, int[] tl, int tl_index, int[] td, int td_index, ZStream z) - { - mode = START; - lbits = (byte) bl; - dbits = (byte) bd; - ltree = tl; - ltree_index = tl_index; - dtree = td; - dtree_index = td_index; - } - - internal InfCodes(int bl, int bd, int[] tl, int[] td, ZStream z) - { - mode = START; - lbits = (byte) bl; - dbits = (byte) bd; - ltree = tl; - ltree_index = 0; - dtree = td; - dtree_index = 0; - } - - internal int proc(InfBlocks s, ZStream z, int r) - { - int j; // temporary storage - //int[] t; // temporary pointer - int tindex; // temporary pointer - int e; // extra bits or operation - int b = 0; // bit buffer - int k = 0; // bits in bit buffer - int p = 0; // input data pointer - int n; // bytes available there - int q; // output window write pointer - int m; // bytes to end of window or read pointer - int f; // pointer to copy strings from - - // copy input/output information to locals (UPDATE macro restores) - p = z.next_in_index; n = z.avail_in; b = s.bitb; k = s.bitk; - q = s.write; m = q < s.read?s.read - q - 1:s.end - q; - - // process input and output based on current state - while (true) - { - switch (mode) - { - - // waiting for "i:"=input, "o:"=output, "x:"=nothing - case START: // x: set up for LEN - if (m >= 258 && n >= 10) - { - - s.bitb = b; s.bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - s.write = q; - r = inflate_fast(lbits, dbits, ltree, ltree_index, dtree, dtree_index, s, z); - - p = z.next_in_index; n = z.avail_in; b = s.bitb; k = s.bitk; - q = s.write; m = q < s.read?s.read - q - 1:s.end - q; - - if (r != Z_OK) - { - mode = r == Z_STREAM_END?WASH:BADCODE; - break; - } - } - need = lbits; - tree = ltree; - tree_index = ltree_index; - - mode = LEN; - goto case LEN; - - case LEN: // i: get length/literal/eob next - j = need; - - while (k < (j)) - { - if (n != 0) - r = Z_OK; - else - { - - s.bitb = b; s.bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - s.write = q; - return s.inflate_flush(z, r); - } - n--; - b |= (z.next_in[p++] & 0xff) << k; - k += 8; - } - - tindex = (tree_index + (b & inflate_mask[j])) * 3; - - b = SupportClass.URShift(b, (tree[tindex + 1])); - k -= (tree[tindex + 1]); - - e = tree[tindex]; - - if (e == 0) - { - // literal - lit = tree[tindex + 2]; - mode = LIT; - break; - } - if ((e & 16) != 0) - { - // length - get_Renamed = e & 15; - len = tree[tindex + 2]; - mode = LENEXT; - break; - } - if ((e & 64) == 0) - { - // next table - need = e; - tree_index = tindex / 3 + tree[tindex + 2]; - break; - } - if ((e & 32) != 0) - { - // end of block - mode = WASH; - break; - } - mode = BADCODE; // invalid code - z.msg = "invalid literal/length code"; - r = Z_DATA_ERROR; - - s.bitb = b; s.bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - s.write = q; - return s.inflate_flush(z, r); - - - case LENEXT: // i: getting length extra (have base) - j = get_Renamed; - - while (k < (j)) - { - if (n != 0) - r = Z_OK; - else - { - - s.bitb = b; s.bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - s.write = q; - return s.inflate_flush(z, r); - } - n--; b |= (z.next_in[p++] & 0xff) << k; - k += 8; - } - - len += (b & inflate_mask[j]); - - b >>= j; - k -= j; - - need = dbits; - tree = dtree; - tree_index = dtree_index; - mode = DIST; - goto case DIST; - - case DIST: // i: get distance next - j = need; - - while (k < (j)) - { - if (n != 0) - r = Z_OK; - else - { - - s.bitb = b; s.bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - s.write = q; - return s.inflate_flush(z, r); - } - n--; b |= (z.next_in[p++] & 0xff) << k; - k += 8; - } - - tindex = (tree_index + (b & inflate_mask[j])) * 3; - - b >>= tree[tindex + 1]; - k -= tree[tindex + 1]; - - e = (tree[tindex]); - if ((e & 16) != 0) - { - // distance - get_Renamed = e & 15; - dist = tree[tindex + 2]; - mode = DISTEXT; - break; - } - if ((e & 64) == 0) - { - // next table - need = e; - tree_index = tindex / 3 + tree[tindex + 2]; - break; - } - mode = BADCODE; // invalid code - z.msg = "invalid distance code"; - r = Z_DATA_ERROR; - - s.bitb = b; s.bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - s.write = q; - return s.inflate_flush(z, r); - - - case DISTEXT: // i: getting distance extra - j = get_Renamed; - - while (k < (j)) - { - if (n != 0) - r = Z_OK; - else - { - - s.bitb = b; s.bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - s.write = q; - return s.inflate_flush(z, r); - } - n--; b |= (z.next_in[p++] & 0xff) << k; - k += 8; - } - - dist += (b & inflate_mask[j]); - - b >>= j; - k -= j; - - mode = COPY; - goto case COPY; - - case COPY: // o: copying bytes in window, waiting for space - f = q - dist; - while (f < 0) - { - // modulo window size-"while" instead - f += s.end; // of "if" handles invalid distances - } - while (len != 0) - { - - if (m == 0) - { - if (q == s.end && s.read != 0) - { - q = 0; m = q < s.read?s.read - q - 1:s.end - q; - } - if (m == 0) - { - s.write = q; r = s.inflate_flush(z, r); - q = s.write; m = q < s.read?s.read - q - 1:s.end - q; - - if (q == s.end && s.read != 0) - { - q = 0; m = q < s.read?s.read - q - 1:s.end - q; - } - - if (m == 0) - { - s.bitb = b; s.bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - s.write = q; - return s.inflate_flush(z, r); - } - } - } - - s.window[q++] = s.window[f++]; m--; - - if (f == s.end) - f = 0; - len--; - } - mode = START; - break; - - case LIT: // o: got literal, waiting for output space - if (m == 0) - { - if (q == s.end && s.read != 0) - { - q = 0; m = q < s.read?s.read - q - 1:s.end - q; - } - if (m == 0) - { - s.write = q; r = s.inflate_flush(z, r); - q = s.write; m = q < s.read?s.read - q - 1:s.end - q; - - if (q == s.end && s.read != 0) - { - q = 0; m = q < s.read?s.read - q - 1:s.end - q; - } - if (m == 0) - { - s.bitb = b; s.bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - s.write = q; - return s.inflate_flush(z, r); - } - } - } - r = Z_OK; - - s.window[q++] = (byte) lit; m--; - - mode = START; - break; - - case WASH: // o: got eob, possibly more output - if (k > 7) - { - // return unused byte, if any - k -= 8; - n++; - p--; // can always return one - } - - s.write = q; r = s.inflate_flush(z, r); - q = s.write; m = q < s.read?s.read - q - 1:s.end - q; - - if (s.read != s.write) - { - s.bitb = b; s.bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - s.write = q; - return s.inflate_flush(z, r); - } - mode = END; - goto case END; - - case END: - r = Z_STREAM_END; - s.bitb = b; s.bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - s.write = q; - return s.inflate_flush(z, r); - - - case BADCODE: // x: got error - - r = Z_DATA_ERROR; - - s.bitb = b; s.bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - s.write = q; - return s.inflate_flush(z, r); - - - default: - r = Z_STREAM_ERROR; - - s.bitb = b; s.bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - s.write = q; - return s.inflate_flush(z, r); - - } - } - } - - internal void free(ZStream z) - { - // ZFREE(z, c); - } - - // Called with number of bytes left to write in window at least 258 - // (the maximum string length) and number of input bytes available - // at least ten. The ten bytes are six bytes for the longest length/ - // distance pair plus four bytes for overloading the bit buffer. - - internal int inflate_fast(int bl, int bd, int[] tl, int tl_index, int[] td, int td_index, InfBlocks s, ZStream z) - { - int t; // temporary pointer - int[] tp; // temporary pointer - int tp_index; // temporary pointer - int e; // extra bits or operation - int b; // bit buffer - int k; // bits in bit buffer - int p; // input data pointer - int n; // bytes available there - int q; // output window write pointer - int m; // bytes to end of window or read pointer - int ml; // mask for literal/length tree - int md; // mask for distance tree - int c; // bytes to copy - int d; // distance back to copy from - int r; // copy source pointer - - // load input, output, bit values - p = z.next_in_index; n = z.avail_in; b = s.bitb; k = s.bitk; - q = s.write; m = q < s.read?s.read - q - 1:s.end - q; - - // initialize masks - ml = inflate_mask[bl]; - md = inflate_mask[bd]; - - // do until not enough input or output space for fast loop - do - { - // assume called with m >= 258 && n >= 10 - // get literal/length code - while (k < (20)) - { - // max bits for literal/length code - n--; - b |= (z.next_in[p++] & 0xff) << k; k += 8; - } - - t = b & ml; - tp = tl; - tp_index = tl_index; - if ((e = tp[(tp_index + t) * 3]) == 0) - { - b >>= (tp[(tp_index + t) * 3 + 1]); k -= (tp[(tp_index + t) * 3 + 1]); - - s.window[q++] = (byte) tp[(tp_index + t) * 3 + 2]; - m--; - continue; - } - do - { - - b >>= (tp[(tp_index + t) * 3 + 1]); k -= (tp[(tp_index + t) * 3 + 1]); - - if ((e & 16) != 0) - { - e &= 15; - c = tp[(tp_index + t) * 3 + 2] + ((int) b & inflate_mask[e]); - - b >>= e; k -= e; - - // decode distance base of block to copy - while (k < (15)) - { - // max bits for distance code - n--; - b |= (z.next_in[p++] & 0xff) << k; k += 8; - } - - t = b & md; - tp = td; - tp_index = td_index; - e = tp[(tp_index + t) * 3]; - - do - { - - b >>= (tp[(tp_index + t) * 3 + 1]); k -= (tp[(tp_index + t) * 3 + 1]); - - if ((e & 16) != 0) - { - // get extra bits to add to distance base - e &= 15; - while (k < (e)) - { - // get extra bits (up to 13) - n--; - b |= (z.next_in[p++] & 0xff) << k; k += 8; - } - - d = tp[(tp_index + t) * 3 + 2] + (b & inflate_mask[e]); - - b >>= (e); k -= (e); - - // do the copy - m -= c; - if (q >= d) - { - // offset before dest - // just copy - r = q - d; - if (q - r > 0 && 2 > (q - r)) - { - s.window[q++] = s.window[r++]; c--; // minimum count is three, - s.window[q++] = s.window[r++]; c--; // so unroll loop a little - } - else - { - Array.Copy(s.window, r, s.window, q, 2); - q += 2; r += 2; c -= 2; - } - } - else - { - // else offset after destination - r = q - d; - do - { - r += s.end; // force pointer in window - } - while (r < 0); // covers invalid distances - e = s.end - r; - if (c > e) - { - // if source crosses, - c -= e; // wrapped copy - if (q - r > 0 && e > (q - r)) - { - do - { - s.window[q++] = s.window[r++]; - } - while (--e != 0); - } - else - { - Array.Copy(s.window, r, s.window, q, e); - q += e; r += e; e = 0; - } - r = 0; // copy rest from start of window - } - } - - // copy all or what's left - if (q - r > 0 && c > (q - r)) - { - do - { - s.window[q++] = s.window[r++]; - } - while (--c != 0); - } - else - { - Array.Copy(s.window, r, s.window, q, c); - q += c; r += c; c = 0; - } - break; - } - else if ((e & 64) == 0) - { - t += tp[(tp_index + t) * 3 + 2]; - t += (b & inflate_mask[e]); - e = tp[(tp_index + t) * 3]; - } - else - { - z.msg = "invalid distance code"; - - c = z.avail_in - n; c = (k >> 3) < c?k >> 3:c; n += c; p -= c; k -= (c << 3); - - s.bitb = b; s.bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - s.write = q; - - return Z_DATA_ERROR; - } - } - while (true); - break; - } - - if ((e & 64) == 0) - { - t += tp[(tp_index + t) * 3 + 2]; - t += (b & inflate_mask[e]); - if ((e = tp[(tp_index + t) * 3]) == 0) - { - - b >>= (tp[(tp_index + t) * 3 + 1]); k -= (tp[(tp_index + t) * 3 + 1]); - - s.window[q++] = (byte) tp[(tp_index + t) * 3 + 2]; - m--; - break; - } - } - else if ((e & 32) != 0) - { - - c = z.avail_in - n; c = (k >> 3) < c?k >> 3:c; n += c; p -= c; k -= (c << 3); - - s.bitb = b; s.bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - s.write = q; - - return Z_STREAM_END; - } - else - { - z.msg = "invalid literal/length code"; - - c = z.avail_in - n; c = (k >> 3) < c?k >> 3:c; n += c; p -= c; k -= (c << 3); - - s.bitb = b; s.bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - s.write = q; - - return Z_DATA_ERROR; - } - } - while (true); - } - while (m >= 258 && n >= 10); - - // not enough input or output--restore pointers and return - c = z.avail_in - n; c = (k >> 3) < c?k >> 3:c; n += c; p -= c; k -= (c << 3); - - s.bitb = b; s.bitk = k; - z.avail_in = n; z.total_in += p - z.next_in_index; z.next_in_index = p; - s.write = q; - - return Z_OK; - } - } -} \ No newline at end of file diff --git a/src/Zlib.net/InfTree.cs b/src/Zlib.net/InfTree.cs deleted file mode 100644 index 16e55a1..0000000 --- a/src/Zlib.net/InfTree.cs +++ /dev/null @@ -1,377 +0,0 @@ -// Copyright (c) 2006, ComponentAce -// http://www.componentace.com -// All rights reserved. - -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -// Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -// Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -// Neither the name of ComponentAce nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -/* -Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -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. The names of the authors may not be used to endorse or promote products -derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 JCRAFT, -INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -/* -* This program is based on zlib-1.1.3, so all credit should go authors -* Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) -* and contributors of zlib. -*/ -using System; -namespace ComponentAce.Compression.Libs.zlib -{ - - sealed class InfTree - { - - private const int MANY = 1440; - - private const int Z_OK = 0; - private const int Z_STREAM_END = 1; - private const int Z_NEED_DICT = 2; - private const int Z_ERRNO = - 1; - private const int Z_STREAM_ERROR = - 2; - private const int Z_DATA_ERROR = - 3; - private const int Z_MEM_ERROR = - 4; - private const int Z_BUF_ERROR = - 5; - private const int Z_VERSION_ERROR = - 6; - - internal const int fixed_bl = 9; - internal const int fixed_bd = 5; - - - internal static readonly int[] fixed_tl = new int[]{96, 7, 256, 0, 8, 80, 0, 8, 16, 84, 8, 115, 82, 7, 31, 0, 8, 112, 0, 8, 48, 0, 9, 192, 80, 7, 10, 0, 8, 96, 0, 8, 32, 0, 9, 160, 0, 8, 0, 0, 8, 128, 0, 8, 64, 0, 9, 224, 80, 7, 6, 0, 8, 88, 0, 8, 24, 0, 9, 144, 83, 7, 59, 0, 8, 120, 0, 8, 56, 0, 9, 208, 81, 7, 17, 0, 8, 104, 0, 8, 40, 0, 9, 176, 0, 8, 8, 0, 8, 136, 0, 8, 72, 0, 9, 240, 80, 7, 4, 0, 8, 84, 0, 8, 20, 85, 8, 227, 83, 7, 43, 0, 8, 116, 0, 8, 52, 0, 9, 200, 81, 7, 13, 0, 8, 100, 0, 8, 36, 0, 9, 168, 0, 8, 4, 0, 8, 132, 0, 8, 68, 0, 9, 232, 80, 7, 8, 0, 8, 92, 0, 8, 28, 0, 9, 152, 84, 7, 83, 0, 8, 124, 0, 8, 60, 0, 9, 216, 82, 7, 23, 0, 8, 108, 0, 8, 44, 0, 9, 184, 0, 8, 12, 0, 8, 140, 0, 8, 76, 0, 9, 248, 80, 7, 3, 0, 8, 82, 0, 8, 18, 85, 8, 163, 83, 7, 35, 0, 8, 114, 0, 8, 50, 0, 9, 196, 81, 7, 11, 0, 8, 98, 0, 8, 34, 0, 9, 164, 0, 8, 2, 0, 8, 130, 0, 8, 66, 0, 9, 228, 80, 7, 7, 0, 8, 90, 0, 8, 26, 0, 9, 148, 84, 7, 67, 0, 8, 122, 0, 8, 58, 0, 9, 212, 82, 7, 19, 0, 8, 106, 0, 8, 42, 0, 9, 180, 0, 8, 10, 0, 8, 138, 0, 8, 74, 0, 9, 244, 80, 7, 5, 0, 8, 86, 0, 8, 22, 192, 8, 0, 83, 7, 51, 0, 8, 118, 0, 8, 54, 0, 9, 204, 81, 7, 15, 0, 8, 102, 0, 8, 38, 0, 9, 172, 0, 8, 6, 0, 8, 134, 0, 8, 70, 0, 9, 236, 80, 7, 9, 0, 8, 94, 0, 8, 30, 0, 9, 156, 84, 7, 99, 0, 8, 126, 0, 8, 62, 0, 9, 220, 82, 7, 27, 0, 8, 110, 0, 8, 46, 0, 9, 188, 0, 8, 14, 0, 8, 142, 0, 8, 78, 0, 9, 252, 96, 7, 256, 0, 8, 81, 0, 8, 17, 85, 8, 131, 82, 7, 31, 0, 8, 113, 0, 8, 49, 0, 9, 194, 80, 7, 10, 0, 8, 97, 0, 8, 33, 0, 9, 162, 0, 8, 1, 0, 8, 129, 0, 8, 65, 0, 9, 226, 80, 7, 6, 0, 8, 89, 0, 8, 25, 0, 9, 146, 83, 7, 59, 0, 8, 121, 0, 8, 57, 0, 9, 210, 81, 7, 17, 0, 8, 105, 0, 8, 41, 0, 9, 178, 0, 8, 9, 0, 8, 137, 0, 8, 73, 0, 9, 242, 80, 7, 4, 0, 8, 85, 0, 8, 21, 80, 8, 258, 83, 7, 43, 0, 8, 117, 0, 8, 53, 0, 9, 202, 81, 7, 13, 0, 8, 101, 0, 8, 37, 0, 9, 170, 0, 8, 5, 0, 8, 133, 0, 8, 69, 0, 9, 234, 80, 7, 8, 0, 8, 93, 0, 8, 29, 0, 9, 154, 84, 7, 83, 0, 8, 125, 0, 8, 61, 0, 9, 218, 82, 7, 23, 0, 8, 109, 0, 8, 45, 0, 9, 186, - 0, 8, 13, 0, 8, 141, 0, 8, 77, 0, 9, 250, 80, 7, 3, 0, 8, 83, 0, 8, 19, 85, 8, 195, 83, 7, 35, 0, 8, 115, 0, 8, 51, 0, 9, 198, 81, 7, 11, 0, 8, 99, 0, 8, 35, 0, 9, 166, 0, 8, 3, 0, 8, 131, 0, 8, 67, 0, 9, 230, 80, 7, 7, 0, 8, 91, 0, 8, 27, 0, 9, 150, 84, 7, 67, 0, 8, 123, 0, 8, 59, 0, 9, 214, 82, 7, 19, 0, 8, 107, 0, 8, 43, 0, 9, 182, 0, 8, 11, 0, 8, 139, 0, 8, 75, 0, 9, 246, 80, 7, 5, 0, 8, 87, 0, 8, 23, 192, 8, 0, 83, 7, 51, 0, 8, 119, 0, 8, 55, 0, 9, 206, 81, 7, 15, 0, 8, 103, 0, 8, 39, 0, 9, 174, 0, 8, 7, 0, 8, 135, 0, 8, 71, 0, 9, 238, 80, 7, 9, 0, 8, 95, 0, 8, 31, 0, 9, 158, 84, 7, 99, 0, 8, 127, 0, 8, 63, 0, 9, 222, 82, 7, 27, 0, 8, 111, 0, 8, 47, 0, 9, 190, 0, 8, 15, 0, 8, 143, 0, 8, 79, 0, 9, 254, 96, 7, 256, 0, 8, 80, 0, 8, 16, 84, 8, 115, 82, 7, 31, 0, 8, 112, 0, 8, 48, 0, 9, 193, 80, 7, 10, 0, 8, 96, 0, 8, 32, 0, 9, 161, 0, 8, 0, 0, 8, 128, 0, 8, 64, 0, 9, 225, 80, 7, 6, 0, 8, 88, 0, 8, 24, 0, 9, 145, 83, 7, 59, 0, 8, 120, 0, 8, 56, 0, 9, 209, 81, 7, 17, 0, 8, 104, 0, 8, 40, 0, 9, 177, 0, 8, 8, 0, 8, 136, 0, 8, 72, 0, 9, 241, 80, 7, 4, 0, 8, 84, 0, 8, 20, 85, 8, 227, 83, 7, 43, 0, 8, 116, 0, 8, 52, 0, 9, 201, 81, 7, 13, 0, 8, 100, 0, 8, 36, 0, 9, 169, 0, 8, 4, 0, 8, 132, 0, 8, 68, 0, 9, 233, 80, 7, 8, 0, 8, 92, 0, 8, 28, 0, 9, 153, 84, 7, 83, 0, 8, 124, 0, 8, 60, 0, 9, 217, 82, 7, 23, 0, 8, 108, 0, 8, 44, 0, 9, 185, 0, 8, 12, 0, 8, 140, 0, 8, 76, 0, 9, 249, 80, 7, 3, 0, 8, 82, 0, 8, 18, 85, 8, 163, 83, 7, 35, 0, 8, 114, 0, 8, 50, 0, 9, 197, 81, 7, 11, 0, 8, 98, 0, 8, 34, 0, 9, 165, 0, 8, 2, 0, 8, 130, 0, 8, 66, 0, 9, 229, 80, 7, 7, 0, 8, 90, 0, 8, 26, 0, 9, 149, 84, 7, 67, 0, 8, 122, 0, 8, 58, 0, 9, 213, 82, 7, 19, 0, 8, 106, 0, 8, 42, 0, 9, 181, 0, 8, 10, 0, 8, 138, 0, 8, 74, 0, 9, 245, 80, 7, 5, 0, 8, 86, 0, 8, 22, 192, 8, 0, 83, 7, 51, 0, 8, 118, 0, 8, 54, 0, 9, 205, 81, 7, 15, 0, 8, 102, 0, 8, 38, 0, 9, 173, 0, 8, 6, 0, 8, 134, 0, 8, 70, 0, 9, 237, 80, 7, 9, 0, 8, 94, 0, 8, 30, 0, 9, 157, 84, 7, 99, 0, 8, 126, 0, 8, 62, 0, 9, 221, 82, 7, 27, 0, 8, 110, 0, 8, 46, 0, 9, 189, 0, 8, - 14, 0, 8, 142, 0, 8, 78, 0, 9, 253, 96, 7, 256, 0, 8, 81, 0, 8, 17, 85, 8, 131, 82, 7, 31, 0, 8, 113, 0, 8, 49, 0, 9, 195, 80, 7, 10, 0, 8, 97, 0, 8, 33, 0, 9, 163, 0, 8, 1, 0, 8, 129, 0, 8, 65, 0, 9, 227, 80, 7, 6, 0, 8, 89, 0, 8, 25, 0, 9, 147, 83, 7, 59, 0, 8, 121, 0, 8, 57, 0, 9, 211, 81, 7, 17, 0, 8, 105, 0, 8, 41, 0, 9, 179, 0, 8, 9, 0, 8, 137, 0, 8, 73, 0, 9, 243, 80, 7, 4, 0, 8, 85, 0, 8, 21, 80, 8, 258, 83, 7, 43, 0, 8, 117, 0, 8, 53, 0, 9, 203, 81, 7, 13, 0, 8, 101, 0, 8, 37, 0, 9, 171, 0, 8, 5, 0, 8, 133, 0, 8, 69, 0, 9, 235, 80, 7, 8, 0, 8, 93, 0, 8, 29, 0, 9, 155, 84, 7, 83, 0, 8, 125, 0, 8, 61, 0, 9, 219, 82, 7, 23, 0, 8, 109, 0, 8, 45, 0, 9, 187, 0, 8, 13, 0, 8, 141, 0, 8, 77, 0, 9, 251, 80, 7, 3, 0, 8, 83, 0, 8, 19, 85, 8, 195, 83, 7, 35, 0, 8, 115, 0, 8, 51, 0, 9, 199, 81, 7, 11, 0, 8, 99, 0, 8, 35, 0, 9, 167, 0, 8, 3, 0, 8, 131, 0, 8, 67, 0, 9, 231, 80, 7, 7, 0, 8, 91, 0, 8, 27, 0, 9, 151, 84, 7, 67, 0, 8, 123, 0, 8, 59, 0, 9, 215, 82, 7, 19, 0, 8, 107, 0, 8, 43, 0, 9, 183, 0, 8, 11, 0, 8, 139, 0, 8, 75, 0, 9, 247, 80, 7, 5, 0, 8, 87, 0, 8, 23, 192, 8, 0, 83, 7, 51, 0, 8, 119, 0, 8, 55, 0, 9, 207, 81, 7, 15, 0, 8, 103, 0, 8, 39, 0, 9, 175, 0, 8, 7, 0, 8, 135, 0, 8, 71, 0, 9, 239, 80, 7, 9, 0, 8, 95, 0, 8, 31, 0, 9, 159, 84, 7, 99, 0, 8, 127, 0, 8, 63, 0, 9, 223, 82, 7, 27, 0, 8, 111, 0, 8, 47, 0, 9, 191, 0, 8, 15, 0, 8, 143, 0, 8, 79, 0, 9, 255}; - - internal static readonly int[] fixed_td = new int[]{80, 5, 1, 87, 5, 257, 83, 5, 17, 91, 5, 4097, 81, 5, 5, 89, 5, 1025, 85, 5, 65, 93, 5, 16385, 80, 5, 3, 88, 5, 513, 84, 5, 33, 92, 5, 8193, 82, 5, 9, 90, 5, 2049, 86, 5, 129, 192, 5, 24577, 80, 5, 2, 87, 5, 385, 83, 5, 25, 91, 5, 6145, 81, 5, 7, 89, 5, 1537, 85, 5, 97, 93, 5, 24577, 80, 5, 4, 88, 5, 769, 84, 5, 49, 92, 5, 12289, 82, 5, 13, 90, 5, 3073, 86, 5, 193, 192, 5, 24577}; - - // Tables for deflate from PKZIP's appnote.txt. - internal static readonly int[] cplens = new int[]{3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0}; - - internal static readonly int[] cplext = new int[]{0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 112, 112}; - - internal static readonly int[] cpdist = new int[]{1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577}; - - internal static readonly int[] cpdext = new int[]{0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13}; - - // If BMAX needs to be larger than 16, then h and x[] should be uLong. - internal const int BMAX = 15; // maximum bit length of any code - - internal static int huft_build(int[] b, int bindex, int n, int s, int[] d, int[] e, int[] t, int[] m, int[] hp, int[] hn, int[] v) - { - // Given a list of code lengths and a maximum table size, make a set of - // tables to decode that set of codes. Return Z_OK on success, Z_BUF_ERROR - // if the given code set is incomplete (the tables are still built in this - // case), Z_DATA_ERROR if the input is invalid (an over-subscribed set of - // lengths), or Z_MEM_ERROR if not enough memory. - - int a; // counter for codes of length k - int[] c = new int[BMAX + 1]; // bit length count table - int f; // i repeats in table every f entries - int g; // maximum code length - int h; // table level - int i; // counter, current code - int j; // counter - int k; // number of bits in current code - int l; // bits per table (returned in m) - int mask; // (1 << w) - 1, to avoid cc -O bug on HP - int p; // pointer into c[], b[], or v[] - int q; // points to current table - int[] r = new int[3]; // table entry for structure assignment - int[] u = new int[BMAX]; // table stack - int w; // bits before this table == (l * h) - int[] x = new int[BMAX + 1]; // bit offsets, then code stack - int xp; // pointer into x - int y; // number of dummy codes added - int z; // number of entries in current table - - // Generate counts for each bit length - - p = 0; i = n; - do - { - c[b[bindex + p]]++; p++; i--; // assume all entries <= BMAX - } - while (i != 0); - - if (c[0] == n) - { - // null input--all zero length codes - t[0] = - 1; - m[0] = 0; - return Z_OK; - } - - // Find minimum and maximum length, bound *m by those - l = m[0]; - for (j = 1; j <= BMAX; j++) - if (c[j] != 0) - break; - k = j; // minimum code length - if (l < j) - { - l = j; - } - for (i = BMAX; i != 0; i--) - { - if (c[i] != 0) - break; - } - g = i; // maximum code length - if (l > i) - { - l = i; - } - m[0] = l; - - // Adjust last length count to fill out codes, if needed - for (y = 1 << j; j < i; j++, y <<= 1) - { - if ((y -= c[j]) < 0) - { - return Z_DATA_ERROR; - } - } - if ((y -= c[i]) < 0) - { - return Z_DATA_ERROR; - } - c[i] += y; - - // Generate starting offsets into the value table for each length - x[1] = j = 0; - p = 1; xp = 2; - while (--i != 0) - { - // note that i == g from above - x[xp] = (j += c[p]); - xp++; - p++; - } - - // Make a table of values in order of bit lengths - i = 0; p = 0; - do - { - if ((j = b[bindex + p]) != 0) - { - v[x[j]++] = i; - } - p++; - } - while (++i < n); - n = x[g]; // set n to length of v - - // Generate the Huffman codes and for each, make the table entries - x[0] = i = 0; // first Huffman code is zero - p = 0; // grab values in bit order - h = - 1; // no tables yet--level -1 - w = - l; // bits decoded == (l * h) - u[0] = 0; // just to keep compilers happy - q = 0; // ditto - z = 0; // ditto - - // go through the bit lengths (k already is bits in shortest code) - for (; k <= g; k++) - { - a = c[k]; - while (a-- != 0) - { - // here i is the Huffman code of length k bits for value *p - // make tables up to required level - while (k > w + l) - { - h++; - w += l; // previous table always l bits - // compute minimum size table less than or equal to l bits - z = g - w; - z = (z > l)?l:z; // table size upper limit - if ((f = 1 << (j = k - w)) > a + 1) - { - // try a k-w bit table - // too few codes for k-w bit table - f -= (a + 1); // deduct codes from patterns left - xp = k; - if (j < z) - { - while (++j < z) - { - // try smaller tables up to z bits - if ((f <<= 1) <= c[++xp]) - break; // enough codes to use up j bits - f -= c[xp]; // else deduct codes from patterns - } - } - } - z = 1 << j; // table entries for j-bit table - - // allocate new table - if (hn[0] + z > MANY) - // (note: doesn't matter for fixed) - return Z_DATA_ERROR; // overflow of MANY - u[h] = q = hn[0]; // DEBUG - hn[0] += z; - - // connect to last table, if there is one - if (h != 0) - { - x[h] = i; // save pattern for backing up - r[0] = (byte) j; // bits in this table - r[1] = (byte) l; // bits to dump before this table - j = SupportClass.URShift(i, (w - l)); - r[2] = (int) (q - u[h - 1] - j); // offset to this table - Array.Copy(r, 0, hp, (u[h - 1] + j) * 3, 3); // connect to last table - } - else - { - t[0] = q; // first table is returned result - } - } - - // set up table entry in r - r[1] = (byte) (k - w); - if (p >= n) - { - r[0] = 128 + 64; // out of values--invalid code - } - else if (v[p] < s) - { - r[0] = (byte) (v[p] < 256?0:32 + 64); // 256 is end-of-block - r[2] = v[p++]; // simple code is just the value - } - else - { - r[0] = (byte) (e[v[p] - s] + 16 + 64); // non-simple--look up in lists - r[2] = d[v[p++] - s]; - } - - // fill code-like entries with r - f = 1 << (k - w); - for (j = SupportClass.URShift(i, w); j < z; j += f) - { - Array.Copy(r, 0, hp, (q + j) * 3, 3); - } - - // backwards increment the k-bit code i - for (j = 1 << (k - 1); (i & j) != 0; j = SupportClass.URShift(j, 1)) - { - i ^= j; - } - i ^= j; - - // backup over finished tables - mask = (1 << w) - 1; // needed on HP, cc -O bug - while ((i & mask) != x[h]) - { - h--; // don't need to update q - w -= l; - mask = (1 << w) - 1; - } - } - } - // Return Z_BUF_ERROR if we were given an incomplete table - return y != 0 && g != 1?Z_BUF_ERROR:Z_OK; - } - - internal static int inflate_trees_bits(int[] c, int[] bb, int[] tb, int[] hp, ZStream z) - { - int r; - int[] hn = new int[1]; // hufts used in space - int[] v = new int[19]; // work area for huft_build - - r = huft_build(c, 0, 19, 19, null, null, tb, bb, hp, hn, v); - - if (r == Z_DATA_ERROR) - { - z.msg = "oversubscribed dynamic bit lengths tree"; - } - else if (r == Z_BUF_ERROR || bb[0] == 0) - { - z.msg = "incomplete dynamic bit lengths tree"; - r = Z_DATA_ERROR; - } - return r; - } - - internal static int inflate_trees_dynamic(int nl, int nd, int[] c, int[] bl, int[] bd, int[] tl, int[] td, int[] hp, ZStream z) - { - int r; - int[] hn = new int[1]; // hufts used in space - int[] v = new int[288]; // work area for huft_build - - // build literal/length tree - r = huft_build(c, 0, nl, 257, cplens, cplext, tl, bl, hp, hn, v); - if (r != Z_OK || bl[0] == 0) - { - if (r == Z_DATA_ERROR) - { - z.msg = "oversubscribed literal/length tree"; - } - else if (r != Z_MEM_ERROR) - { - z.msg = "incomplete literal/length tree"; - r = Z_DATA_ERROR; - } - return r; - } - - // build distance tree - r = huft_build(c, nl, nd, 0, cpdist, cpdext, td, bd, hp, hn, v); - - if (r != Z_OK || (bd[0] == 0 && nl > 257)) - { - if (r == Z_DATA_ERROR) - { - z.msg = "oversubscribed distance tree"; - } - else if (r == Z_BUF_ERROR) - { - z.msg = "incomplete distance tree"; - r = Z_DATA_ERROR; - } - else if (r != Z_MEM_ERROR) - { - z.msg = "empty distance tree with lengths"; - r = Z_DATA_ERROR; - } - return r; - } - - return Z_OK; - } - - internal static int inflate_trees_fixed(int[] bl, int[] bd, int[][] tl, int[][] td, ZStream z) - { - bl[0] = fixed_bl; - bd[0] = fixed_bd; - tl[0] = fixed_tl; - td[0] = fixed_td; - return Z_OK; - } - } -} \ No newline at end of file diff --git a/src/Zlib.net/Inflate.cs b/src/Zlib.net/Inflate.cs deleted file mode 100644 index d7d8fa3..0000000 --- a/src/Zlib.net/Inflate.cs +++ /dev/null @@ -1,450 +0,0 @@ -// Copyright (c) 2006, ComponentAce -// http://www.componentace.com -// All rights reserved. - -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -// Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -// Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -// Neither the name of ComponentAce nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -/* -Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -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. The names of the authors may not be used to endorse or promote products -derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 JCRAFT, -INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -/* -* This program is based on zlib-1.1.3, so all credit should go authors -* Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) -* and contributors of zlib. -*/ -using System; -namespace ComponentAce.Compression.Libs.zlib -{ - - sealed class Inflate - { - - private const int MAX_WBITS = 15; // 32K LZ77 window - - // preset dictionary flag in zlib header - private const int PRESET_DICT = 0x20; - - internal const int Z_NO_FLUSH = 0; - internal const int Z_PARTIAL_FLUSH = 1; - internal const int Z_SYNC_FLUSH = 2; - internal const int Z_FULL_FLUSH = 3; - internal const int Z_FINISH = 4; - - private const int Z_DEFLATED = 8; - - private const int Z_OK = 0; - private const int Z_STREAM_END = 1; - private const int Z_NEED_DICT = 2; - private const int Z_ERRNO = - 1; - private const int Z_STREAM_ERROR = - 2; - private const int Z_DATA_ERROR = - 3; - private const int Z_MEM_ERROR = - 4; - private const int Z_BUF_ERROR = - 5; - private const int Z_VERSION_ERROR = - 6; - - private const int METHOD = 0; // waiting for method byte - private const int FLAG = 1; // waiting for flag byte - private const int DICT4 = 2; // four dictionary check bytes to go - private const int DICT3 = 3; // three dictionary check bytes to go - private const int DICT2 = 4; // two dictionary check bytes to go - private const int DICT1 = 5; // one dictionary check byte to go - private const int DICT0 = 6; // waiting for inflateSetDictionary - private const int BLOCKS = 7; // decompressing blocks - private const int CHECK4 = 8; // four check bytes to go - private const int CHECK3 = 9; // three check bytes to go - private const int CHECK2 = 10; // two check bytes to go - private const int CHECK1 = 11; // one check byte to go - private const int DONE = 12; // finished check, done - private const int BAD = 13; // got an error--stay here - - internal int mode; // current inflate mode - - // mode dependent information - internal int method; // if FLAGS, method byte - - // if CHECK, check values to compare - internal long[] was = new long[1]; // computed check value - internal long need; // stream check value - - // if BAD, inflateSync's marker bytes count - internal int marker; - - // mode independent information - internal int nowrap; // flag for no wrapper - internal int wbits; // log2(window size) (8..15, defaults to 15) - - internal InfBlocks blocks; // current inflate_blocks state - - internal int inflateReset(ZStream z) - { - if (z == null || z.istate == null) - return Z_STREAM_ERROR; - - z.total_in = z.total_out = 0; - z.msg = null; - z.istate.mode = z.istate.nowrap != 0?BLOCKS:METHOD; - z.istate.blocks.reset(z, null); - return Z_OK; - } - - internal int inflateEnd(ZStream z) - { - if (blocks != null) - blocks.free(z); - blocks = null; - // ZFREE(z, z->state); - return Z_OK; - } - - internal int inflateInit(ZStream z, int w) - { - z.msg = null; - blocks = null; - - // handle undocumented nowrap option (no zlib header or check) - nowrap = 0; - if (w < 0) - { - w = - w; - nowrap = 1; - } - - // set window size - if (w < 8 || w > 15) - { - inflateEnd(z); - return Z_STREAM_ERROR; - } - wbits = w; - - z.istate.blocks = new InfBlocks(z, z.istate.nowrap != 0?null:this, 1 << w); - - // reset state - inflateReset(z); - return Z_OK; - } - - internal int inflate(ZStream z, int f) - { - int r; - int b; - - if (z == null || z.istate == null || z.next_in == null) - return Z_STREAM_ERROR; - f = f == Z_FINISH?Z_BUF_ERROR:Z_OK; - r = Z_BUF_ERROR; - while (true) - { - //System.out.println("mode: "+z.istate.mode); - switch (z.istate.mode) - { - - case METHOD: - - if (z.avail_in == 0) - return r; r = f; - - z.avail_in--; z.total_in++; - if (((z.istate.method = z.next_in[z.next_in_index++]) & 0xf) != Z_DEFLATED) - { - z.istate.mode = BAD; - z.msg = "unknown compression method"; - z.istate.marker = 5; // can't try inflateSync - break; - } - if ((z.istate.method >> 4) + 8 > z.istate.wbits) - { - z.istate.mode = BAD; - z.msg = "invalid window size"; - z.istate.marker = 5; // can't try inflateSync - break; - } - z.istate.mode = FLAG; - goto case FLAG; - - case FLAG: - - if (z.avail_in == 0) - return r; r = f; - - z.avail_in--; z.total_in++; - b = (z.next_in[z.next_in_index++]) & 0xff; - - if ((((z.istate.method << 8) + b) % 31) != 0) - { - z.istate.mode = BAD; - z.msg = "incorrect header check"; - z.istate.marker = 5; // can't try inflateSync - break; - } - - if ((b & PRESET_DICT) == 0) - { - z.istate.mode = BLOCKS; - break; - } - z.istate.mode = DICT4; - goto case DICT4; - - case DICT4: - - if (z.avail_in == 0) - return r; r = f; - - z.avail_in--; z.total_in++; - z.istate.need = ((long)(z.next_in[z.next_in_index++] & 0xff) << 24) & unchecked((int) 0xff000000L); - z.istate.mode = DICT3; - goto case DICT3; - - case DICT3: - - if (z.avail_in == 0) - return r; r = f; - - z.avail_in--; z.total_in++; - z.istate.need += (((long)(z.next_in[z.next_in_index++] & 0xff) << 16) & 0xff0000L); - z.istate.mode = DICT2; - goto case DICT2; - - case DICT2: - - if (z.avail_in == 0) - return r; r = f; - - z.avail_in--; z.total_in++; - z.istate.need += (((long)(z.next_in[z.next_in_index++] & 0xff) << 8) & 0xff00L); - z.istate.mode = DICT1; - goto case DICT1; - - case DICT1: - - if (z.avail_in == 0) - return r; r = f; - - z.avail_in--; z.total_in++; - z.istate.need += (long)(z.next_in[z.next_in_index++] & 0xffL); - z.adler = z.istate.need; - z.istate.mode = DICT0; - return Z_NEED_DICT; - - case DICT0: - z.istate.mode = BAD; - z.msg = "need dictionary"; - z.istate.marker = 0; // can try inflateSync - return Z_STREAM_ERROR; - - case BLOCKS: - - r = z.istate.blocks.proc(z, r); - if (r == Z_DATA_ERROR) - { - z.istate.mode = BAD; - z.istate.marker = 0; // can try inflateSync - break; - } - if (r == Z_OK) - { - r = f; - } - if (r != Z_STREAM_END) - { - return r; - } - r = f; - z.istate.blocks.reset(z, z.istate.was); - if (z.istate.nowrap != 0) - { - z.istate.mode = DONE; - break; - } - z.istate.mode = CHECK4; - goto case CHECK4; - - case CHECK4: - - if (z.avail_in == 0) - return r; r = f; - - z.avail_in--; z.total_in++; - z.istate.need = ((z.next_in[z.next_in_index++] & 0xff) << 24) & unchecked((int) 0xff000000L); - z.istate.mode = CHECK3; - goto case CHECK3; - - case CHECK3: - - if (z.avail_in == 0) - return r; r = f; - - z.avail_in--; z.total_in++; - z.istate.need += (((z.next_in[z.next_in_index++] & 0xff) << 16) & 0xff0000L); - z.istate.mode = CHECK2; - goto case CHECK2; - - case CHECK2: - - if (z.avail_in == 0) - return r; r = f; - - z.avail_in--; z.total_in++; - z.istate.need += (((z.next_in[z.next_in_index++] & 0xff) << 8) & 0xff00L); - z.istate.mode = CHECK1; - goto case CHECK1; - - case CHECK1: - - if (z.avail_in == 0) - return r; r = f; - - z.avail_in--; z.total_in++; - z.istate.need += (z.next_in[z.next_in_index++] & 0xffL); - - if (((int) (z.istate.was[0])) != ((int) (z.istate.need))) - { - z.istate.mode = BAD; - z.msg = "incorrect data check"; - z.istate.marker = 5; // can't try inflateSync - break; - } - - z.istate.mode = DONE; - goto case DONE; - - case DONE: - return Z_STREAM_END; - - case BAD: - return Z_DATA_ERROR; - - default: - return Z_STREAM_ERROR; - - } - } - } - - - internal int inflateSetDictionary(ZStream z, byte[] dictionary, int dictLength) - { - int index = 0; - int length = dictLength; - if (z == null || z.istate == null || z.istate.mode != DICT0) - return Z_STREAM_ERROR; - - if (z._adler.adler32(1L, dictionary, 0, dictLength) != z.adler) - { - return Z_DATA_ERROR; - } - - z.adler = z._adler.adler32(0, null, 0, 0); - - if (length >= (1 << z.istate.wbits)) - { - length = (1 << z.istate.wbits) - 1; - index = dictLength - length; - } - z.istate.blocks.set_dictionary(dictionary, index, length); - z.istate.mode = BLOCKS; - return Z_OK; - } - - private static byte[] mark = new byte[]{(byte) 0, (byte) 0, (byte) SupportClass.Identity(0xff), (byte) SupportClass.Identity(0xff)}; - - internal int inflateSync(ZStream z) - { - int n; // number of bytes to look at - int p; // pointer to bytes - int m; // number of marker bytes found in a row - long r, w; // temporaries to save total_in and total_out - - // set up - if (z == null || z.istate == null) - return Z_STREAM_ERROR; - if (z.istate.mode != BAD) - { - z.istate.mode = BAD; - z.istate.marker = 0; - } - if ((n = z.avail_in) == 0) - return Z_BUF_ERROR; - p = z.next_in_index; - m = z.istate.marker; - - // search - while (n != 0 && m < 4) - { - if (z.next_in[p] == mark[m]) - { - m++; - } - else if (z.next_in[p] != 0) - { - m = 0; - } - else - { - m = 4 - m; - } - p++; n--; - } - - // restore - z.total_in += p - z.next_in_index; - z.next_in_index = p; - z.avail_in = n; - z.istate.marker = m; - - // return no joy or set up to restart on a new block - if (m != 4) - { - return Z_DATA_ERROR; - } - r = z.total_in; w = z.total_out; - inflateReset(z); - z.total_in = r; z.total_out = w; - z.istate.mode = BLOCKS; - return Z_OK; - } - - // Returns true if inflate is currently at the end of a block generated - // by Z_SYNC_FLUSH or Z_FULL_FLUSH. This function is used by one PPP - // implementation to provide an additional safety check. PPP uses Z_SYNC_FLUSH - // but removes the length bytes of the resulting empty stored block. When - // decompressing, PPP checks that at the end of input packet, inflate is - // waiting for these length bytes. - internal int inflateSyncPoint(ZStream z) - { - if (z == null || z.istate == null || z.istate.blocks == null) - return Z_STREAM_ERROR; - return z.istate.blocks.sync_point(); - } - } -} \ No newline at end of file diff --git a/src/Zlib.net/LICENSE b/src/Zlib.net/LICENSE deleted file mode 100644 index afe88e5..0000000 --- a/src/Zlib.net/LICENSE +++ /dev/null @@ -1,10 +0,0 @@ -Copyright (c) 2006-2007, ComponentAce -http://www.componentace.com -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -Neither the name of ComponentAce nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/Zlib.net/README.md b/src/Zlib.net/README.md deleted file mode 100644 index cd11688..0000000 --- a/src/Zlib.net/README.md +++ /dev/null @@ -1,84 +0,0 @@ -ZLIB.NET: README -================================================== - -Contents --------- - - Program information - Company information - Description - Specification - Other ComponentAce compression products - - - -Program information -------------------- - -Program Name: - ZLIB.NET -License Type: freeware - -Program Version: - 1.04 -Program Release Date: - 03/28/2007 -Program Purpose: - version of ZLIB compression library for .NET framework -Target Environment: - Visual Studio 2003, Visual Studio 2005, Borland Developer Studio 2005, Borland Developer Studio 2006 and other - - -Company information -------------------- - -Company Name: - ComponentAce -Contact E-mail Address: - support@componentace.com -Contact WWW URL: - http://www.componentace.com - - -Description ------------ - -100% managed version of ZLIB compression library. -Based on JZlib library (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. - -The zlib compression library was written by Jean-loup Gailly gzip@prep.ai.mit.edu and Mark Adler madler@alumni.caltech.edu. - -The primary site for the zlib compression library is http://www.zlib.org. - -Copyright and license ---------------------- - -See "LICENSE" file. - - -Warranty and guarantee ----------------------- - -See "LICENSE" file. - - -Other ComponentAce compression products ---------------------------------------- - -ZipForge.NET ------------- - -ZipForge.NET is an advanced ZIP compression library for .NET framework. -ZipForge.NET features include streaming support, transaction system, ZIP encryption, repair, -progress indication, Zip64 support, SFX (self-extracting) archives, unicode filenames, spanning support and much more. - -FlexCompress.NET ----------------- - -FlexCompress.NET is an advanced compression and encryption .NET component designed to provide archive -functionality for your applications. This solution provides flexible compression and strong encryption algorithms that -allows you to integrate archiving or backup features into your programs in a fast and easy way. -FlexCompres.NET uses its own file format which allows to achieve high compression rate. - -For more info visit -http://www.componentace.com/.NET_components \ No newline at end of file diff --git a/src/Zlib.net/StaticTree.cs b/src/Zlib.net/StaticTree.cs deleted file mode 100644 index da83fb8..0000000 --- a/src/Zlib.net/StaticTree.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) 2006, ComponentAce -// http://www.componentace.com -// All rights reserved. - -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -// Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -// Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -// Neither the name of ComponentAce nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -/* -Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -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. The names of the authors may not be used to endorse or promote products -derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 JCRAFT, -INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -/* -* This program is based on zlib-1.1.3, so all credit should go authors -* Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) -* and contributors of zlib. -*/ -using System; -namespace ComponentAce.Compression.Libs.zlib -{ - - sealed class StaticTree - { - private const int MAX_BITS = 15; - - private const int BL_CODES = 19; - private const int D_CODES = 30; - private const int LITERALS = 256; - private const int LENGTH_CODES = 29; - private static readonly int L_CODES = (LITERALS + 1 + LENGTH_CODES); - - // Bit length codes must not exceed MAX_BL_BITS bits - internal const int MAX_BL_BITS = 7; - - internal static readonly short[] static_ltree = new short[]{12, 8, 140, 8, 76, 8, 204, 8, 44, 8, 172, 8, 108, 8, 236, 8, 28, 8, 156, 8, 92, 8, 220, 8, 60, 8, 188, 8, 124, 8, 252, 8, 2, 8, 130, 8, 66, 8, 194, 8, 34, 8, 162, 8, 98, 8, 226, 8, 18, 8, 146, 8, 82, 8, 210, 8, 50, 8, 178, 8, 114, 8, 242, 8, 10, 8, 138, 8, 74, 8, 202, 8, 42, 8, 170, 8, 106, 8, 234, 8, 26, 8, 154, 8, 90, 8, 218, 8, 58, 8, 186, 8, 122, 8, 250, 8, 6, 8, 134, 8, 70, 8, 198, 8, 38, 8, 166, 8, 102, 8, 230, 8, 22, 8, 150, 8, 86, 8, 214, 8, 54, 8, 182, 8, 118, 8, 246, 8, 14, 8, 142, 8, 78, 8, 206, 8, 46, 8, 174, 8, 110, 8, 238, 8, 30, 8, 158, 8, 94, 8, 222, 8, 62, 8, 190, 8, 126, 8, 254, 8, 1, 8, 129, 8, 65, 8, 193, 8, 33, 8, 161, 8, 97, 8, 225, 8, 17, 8, 145, 8, 81, 8, 209, 8, 49, 8, 177, 8, 113, 8, 241, 8, 9, 8, 137, 8, 73, 8, 201, 8, 41, 8, 169, 8, 105, 8, 233, 8, 25, 8, 153, 8, 89, 8, 217, 8, 57, 8, 185, 8, 121, 8, 249, 8, 5, 8, 133, 8, 69, 8, 197, 8, 37, 8, 165, 8, 101, 8, 229, 8, 21, 8, 149, 8, 85, 8, 213, 8, 53, 8, 181, 8, 117, 8, 245, 8, 13, 8, 141, 8, 77, 8, 205, 8, 45, 8, 173, 8, 109, 8, 237, 8, 29, 8, 157, 8, 93, 8, 221, 8, 61, 8, 189, 8, 125, 8, 253, 8, 19, 9, 275, 9, 147, 9, 403, 9, 83, 9, 339, 9, 211, 9, 467, 9, 51, 9, 307, 9, 179, 9, 435, 9, 115, 9, 371, 9, 243, 9, 499, 9, 11, 9, 267, 9, 139, 9, 395, 9, 75, 9, 331, 9, 203, 9, 459, 9, 43, 9, 299, 9, 171, 9, 427, 9, 107, 9, 363, 9, 235, 9, 491, 9, 27, 9, 283, 9, 155, 9, 411, 9, 91, 9, 347, 9, 219, 9, 475, 9, 59, 9, 315, 9, 187, 9, 443, 9, 123, 9, 379, 9, 251, 9, 507, 9, 7, 9, 263, 9, 135, 9, 391, 9, 71, 9, 327, 9, 199, 9, 455, 9, 39, 9, 295, 9, 167, 9, 423, 9, 103, 9, 359, 9, 231, 9, 487, 9, 23, 9, 279, 9, 151, 9, 407, 9, 87, 9, 343, 9, 215, 9, 471, 9, 55, 9, 311, 9, 183, 9, 439, 9, 119, 9, 375, 9, 247, 9, 503, 9, 15, 9, 271, 9, 143, 9, 399, 9, 79, 9, 335, 9, 207, 9, 463, 9, 47, 9, 303, 9, 175, 9, 431, 9, 111, 9, 367, 9, 239, 9, 495, 9, 31, 9, 287, 9, 159, 9, 415, 9, 95, 9, 351, 9, 223, 9, 479, 9, 63, 9, 319, 9, 191, 9, 447, 9, 127, 9, 383, 9, 255, 9, 511, 9, 0, 7, 64, 7 - , 32, 7, 96, 7, 16, 7, 80, 7, 48, 7, 112, 7, 8, 7, 72, 7, 40, 7, 104, 7, 24, 7, 88, 7, 56, 7, 120, 7, 4, 7, 68, 7, 36, 7, 100, 7, 20, 7, 84, 7, 52, 7, 116, 7, 3, 8, 131, 8, 67, 8, 195, 8, 35, 8, 163, 8, 99, 8, 227, 8}; - - internal static readonly short[] static_dtree = new short[]{0, 5, 16, 5, 8, 5, 24, 5, 4, 5, 20, 5, 12, 5, 28, 5, 2, 5, 18, 5, 10, 5, 26, 5, 6, 5, 22, 5, 14, 5, 30, 5, 1, 5, 17, 5, 9, 5, 25, 5, 5, 5, 21, 5, 13, 5, 29, 5, 3, 5, 19, 5, 11, 5, 27, 5, 7, 5, 23, 5}; - - internal static StaticTree static_l_desc; - - internal static StaticTree static_d_desc; - - internal static StaticTree static_bl_desc; - - internal short[] static_tree; // static tree or null - internal int[] extra_bits; // extra bits for each code or null - internal int extra_base; // base index for extra_bits - internal int elems; // max number of elements in the tree - internal int max_length; // max bit length for the codes - - internal StaticTree(short[] static_tree, int[] extra_bits, int extra_base, int elems, int max_length) - { - this.static_tree = static_tree; - this.extra_bits = extra_bits; - this.extra_base = extra_base; - this.elems = elems; - this.max_length = max_length; - } - static StaticTree() - { - static_l_desc = new StaticTree(static_ltree, Tree.extra_lbits, LITERALS + 1, L_CODES, MAX_BITS); - static_d_desc = new StaticTree(static_dtree, Tree.extra_dbits, 0, D_CODES, MAX_BITS); - static_bl_desc = new StaticTree(null, Tree.extra_blbits, 0, BL_CODES, MAX_BL_BITS); - } - } -} \ No newline at end of file diff --git a/src/Zlib.net/SupportClass.cs b/src/Zlib.net/SupportClass.cs deleted file mode 100644 index dc6cc2d..0000000 --- a/src/Zlib.net/SupportClass.cs +++ /dev/null @@ -1,171 +0,0 @@ - -using System; - - -namespace ComponentAce.Compression.Libs.zlib -{ - public class SupportClass - { - /// - /// This method returns the literal value received - /// - /// The literal to return - /// The received value - public static long Identity(long literal) - { - return literal; - } - - /// - /// This method returns the literal value received - /// - /// The literal to return - /// The received value - public static ulong Identity(ulong literal) - { - return literal; - } - - /// - /// This method returns the literal value received - /// - /// The literal to return - /// The received value - public static float Identity(float literal) - { - return literal; - } - - /// - /// This method returns the literal value received - /// - /// The literal to return - /// The received value - public static double Identity(double literal) - { - return literal; - } - - /*******************************/ - /// - /// Performs an unsigned bitwise right shift with the specified number - /// - /// Number to operate on - /// Ammount of bits to shift - /// The resulting number from the shift operation - public static int URShift(int number, int bits) - { - if ( number >= 0) - return number >> bits; - else - return (number >> bits) + (2 << ~bits); - } - - /// - /// Performs an unsigned bitwise right shift with the specified number - /// - /// Number to operate on - /// Ammount of bits to shift - /// The resulting number from the shift operation - public static int URShift(int number, long bits) - { - return URShift(number, (int)bits); - } - - /// - /// Performs an unsigned bitwise right shift with the specified number - /// - /// Number to operate on - /// Ammount of bits to shift - /// The resulting number from the shift operation - public static long URShift(long number, int bits) - { - if ( number >= 0) - return number >> bits; - else - return (number >> bits) + (2L << ~bits); - } - - /// - /// Performs an unsigned bitwise right shift with the specified number - /// - /// Number to operate on - /// Ammount of bits to shift - /// The resulting number from the shift operation - public static long URShift(long number, long bits) - { - return URShift(number, (int)bits); - } - - /*******************************/ - /// Reads a number of characters from the current source Stream and writes the data to the target array at the specified index. - /// The source Stream to read from. - /// Contains the array of characteres read from the source Stream. - /// The starting index of the target array. - /// The maximum number of characters to read from the source Stream. - /// The number of characters read. The number will be less than or equal to count depending on the data available in the source Stream. Returns -1 if the end of the stream is reached. - public static System.Int32 ReadInput(System.IO.Stream sourceStream, byte[] target, int start, int count) - { - // Returns 0 bytes if not enough space in target - if (target.Length == 0) - return 0; - - byte[] receiver = new byte[target.Length]; - int bytesRead = sourceStream.Read(receiver, start, count); - - // Returns -1 if EOF - if (bytesRead == 0) - return -1; - - for(int i = start; i < start + bytesRead; i++) - target[i] = (byte)receiver[i]; - - return bytesRead; - } - - /// Reads a number of characters from the current source TextReader and writes the data to the target array at the specified index. - /// The source TextReader to read from - /// Contains the array of characteres read from the source TextReader. - /// The starting index of the target array. - /// The maximum number of characters to read from the source TextReader. - /// The number of characters read. The number will be less than or equal to count depending on the data available in the source TextReader. Returns -1 if the end of the stream is reached. - public static System.Int32 ReadInput(System.IO.TextReader sourceTextReader, byte[] target, int start, int count) - { - // Returns 0 bytes if not enough space in target - if (target.Length == 0) return 0; - - char[] charArray = new char[target.Length]; - int bytesRead = sourceTextReader.Read(charArray, start, count); - - // Returns -1 if EOF - if (bytesRead == 0) return -1; - - for(int index=start; index - /// Converts a string to an array of bytes - /// - /// The string to be converted - /// The new array of bytes - public static byte[] ToByteArray(System.String sourceString) - { - return System.Text.UTF8Encoding.UTF8.GetBytes(sourceString); - } - - /// - /// Converts an array of bytes to an array of chars - /// - /// The array of bytes to convert - /// The new array of chars - public static char[] ToCharArray(byte[] byteArray) - { - return System.Text.UTF8Encoding.UTF8.GetChars(byteArray); - } - - - } -} \ No newline at end of file diff --git a/src/Zlib.net/Tree.cs b/src/Zlib.net/Tree.cs deleted file mode 100644 index 41ea02c..0000000 --- a/src/Zlib.net/Tree.cs +++ /dev/null @@ -1,349 +0,0 @@ -// Copyright (c) 2006, ComponentAce -// http://www.componentace.com -// All rights reserved. - -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -// Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -// Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -// Neither the name of ComponentAce nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -/* -Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -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. The names of the authors may not be used to endorse or promote products -derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 JCRAFT, -INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -/* -* This program is based on zlib-1.1.3, so all credit should go authors -* Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) -* and contributors of zlib. -*/ -using System; -namespace ComponentAce.Compression.Libs.zlib -{ - - sealed class Tree - { - private const int MAX_BITS = 15; - private const int BL_CODES = 19; - private const int D_CODES = 30; - private const int LITERALS = 256; - private const int LENGTH_CODES = 29; - private static readonly int L_CODES = (LITERALS + 1 + LENGTH_CODES); - private static readonly int HEAP_SIZE = (2 * L_CODES + 1); - - // Bit length codes must not exceed MAX_BL_BITS bits - internal const int MAX_BL_BITS = 7; - - // end of block literal code - internal const int END_BLOCK = 256; - - // repeat previous bit length 3-6 times (2 bits of repeat count) - internal const int REP_3_6 = 16; - - // repeat a zero length 3-10 times (3 bits of repeat count) - internal const int REPZ_3_10 = 17; - - // repeat a zero length 11-138 times (7 bits of repeat count) - internal const int REPZ_11_138 = 18; - - // extra bits for each length code - internal static readonly int[] extra_lbits = new int[]{0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0}; - - // extra bits for each distance code - internal static readonly int[] extra_dbits = new int[]{0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13}; - - // extra bits for each bit length code - internal static readonly int[] extra_blbits = new int[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 7}; - - internal static readonly byte[] bl_order = new byte[]{16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}; - - - // The lengths of the bit length codes are sent in order of decreasing - // probability, to avoid transmitting the lengths for unused bit - // length codes. - - internal const int Buf_size = 8 * 2; - - // see definition of array dist_code below - internal const int DIST_CODE_LEN = 512; - - internal static readonly byte[] _dist_code = new byte[]{0, 1, 2, 3, 4, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 16, 17, 18, 18, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, - 29, 29, 29, 29, 29, 29, 29, 29, 29}; - - internal static readonly byte[] _length_code = new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 28}; - - internal static readonly int[] base_length = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 0}; - - internal static readonly int[] base_dist = new int[]{0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096, 6144, 8192, 12288, 16384, 24576}; - - // Mapping from a distance to a distance code. dist is the distance - 1 and - // must not have side effects. _dist_code[256] and _dist_code[257] are never - // used. - internal static int d_code(int dist) - { - return ((dist) < 256?_dist_code[dist]:_dist_code[256 + (SupportClass.URShift((dist), 7))]); - } - - internal short[] dyn_tree; // the dynamic tree - internal int max_code; // largest code with non zero frequency - internal StaticTree stat_desc; // the corresponding static tree - - // Compute the optimal bit lengths for a tree and update the total bit length - // for the current block. - // IN assertion: the fields freq and dad are set, heap[heap_max] and - // above are the tree nodes sorted by increasing frequency. - // OUT assertions: the field len is set to the optimal bit length, the - // array bl_count contains the frequencies for each bit length. - // The length opt_len is updated; static_len is also updated if stree is - // not null. - internal void gen_bitlen(Deflate s) - { - short[] tree = dyn_tree; - short[] stree = stat_desc.static_tree; - int[] extra = stat_desc.extra_bits; - int base_Renamed = stat_desc.extra_base; - int max_length = stat_desc.max_length; - int h; // heap index - int n, m; // iterate over the tree elements - int bits; // bit length - int xbits; // extra bits - short f; // frequency - int overflow = 0; // number of elements with bit length too large - - for (bits = 0; bits <= MAX_BITS; bits++) - s.bl_count[bits] = 0; - - // In a first pass, compute the optimal bit lengths (which may - // overflow in the case of the bit length tree). - tree[s.heap[s.heap_max] * 2 + 1] = 0; // root of the heap - - for (h = s.heap_max + 1; h < HEAP_SIZE; h++) - { - n = s.heap[h]; - bits = tree[tree[n * 2 + 1] * 2 + 1] + 1; - if (bits > max_length) - { - bits = max_length; overflow++; - } - tree[n * 2 + 1] = (short) bits; - // We overwrite tree[n*2+1] which is no longer needed - - if (n > max_code) - continue; // not a leaf node - - s.bl_count[bits]++; - xbits = 0; - if (n >= base_Renamed) - xbits = extra[n - base_Renamed]; - f = tree[n * 2]; - s.opt_len += f * (bits + xbits); - if (stree != null) - s.static_len += f * (stree[n * 2 + 1] + xbits); - } - if (overflow == 0) - return ; - - // This happens for example on obj2 and pic of the Calgary corpus - // Find the first bit length which could increase: - do - { - bits = max_length - 1; - while (s.bl_count[bits] == 0) - bits--; - s.bl_count[bits]--; // move one leaf down the tree - s.bl_count[bits + 1] = (short) (s.bl_count[bits + 1] + 2); // move one overflow item as its brother - s.bl_count[max_length]--; - // The brother of the overflow item also moves one step up, - // but this does not affect bl_count[max_length] - overflow -= 2; - } - while (overflow > 0); - - for (bits = max_length; bits != 0; bits--) - { - n = s.bl_count[bits]; - while (n != 0) - { - m = s.heap[--h]; - if (m > max_code) - continue; - if (tree[m * 2 + 1] != bits) - { - s.opt_len = (int) (s.opt_len + ((long) bits - (long) tree[m * 2 + 1]) * (long) tree[m * 2]); - tree[m * 2 + 1] = (short) bits; - } - n--; - } - } - } - - // Construct one Huffman tree and assigns the code bit strings and lengths. - // Update the total bit length for the current block. - // IN assertion: the field freq is set for all tree elements. - // OUT assertions: the fields len and code are set to the optimal bit length - // and corresponding code. The length opt_len is updated; static_len is - // also updated if stree is not null. The field max_code is set. - internal void build_tree(Deflate s) - { - short[] tree = dyn_tree; - short[] stree = stat_desc.static_tree; - int elems = stat_desc.elems; - int n, m; // iterate over heap elements - int max_code = - 1; // largest code with non zero frequency - int node; // new node being created - - // Construct the initial heap, with least frequent element in - // heap[1]. The sons of heap[n] are heap[2*n] and heap[2*n+1]. - // heap[0] is not used. - s.heap_len = 0; - s.heap_max = HEAP_SIZE; - - for (n = 0; n < elems; n++) - { - if (tree[n * 2] != 0) - { - s.heap[++s.heap_len] = max_code = n; - s.depth[n] = 0; - } - else - { - tree[n * 2 + 1] = 0; - } - } - - // The pkzip format requires that at least one distance code exists, - // and that at least one bit should be sent even if there is only one - // possible code. So to avoid special checks later on we force at least - // two codes of non zero frequency. - while (s.heap_len < 2) - { - node = s.heap[++s.heap_len] = (max_code < 2?++max_code:0); - tree[node * 2] = 1; - s.depth[node] = 0; - s.opt_len--; - if (stree != null) - s.static_len -= stree[node * 2 + 1]; - // node is 0 or 1 so it does not have extra bits - } - this.max_code = max_code; - - // The elements heap[heap_len/2+1 .. heap_len] are leaves of the tree, - // establish sub-heaps of increasing lengths: - - for (n = s.heap_len / 2; n >= 1; n--) - s.pqdownheap(tree, n); - - // Construct the Huffman tree by repeatedly combining the least two - // frequent nodes. - - node = elems; // next internal node of the tree - do - { - // n = node of least frequency - n = s.heap[1]; - s.heap[1] = s.heap[s.heap_len--]; - s.pqdownheap(tree, 1); - m = s.heap[1]; // m = node of next least frequency - - s.heap[--s.heap_max] = n; // keep the nodes sorted by frequency - s.heap[--s.heap_max] = m; - - // Create a new node father of n and m - tree[node * 2] = (short) (tree[n * 2] + tree[m * 2]); - s.depth[node] = (byte) (System.Math.Max((byte) s.depth[n], (byte) s.depth[m]) + 1); - tree[n * 2 + 1] = tree[m * 2 + 1] = (short) node; - - // and insert the new node in the heap - s.heap[1] = node++; - s.pqdownheap(tree, 1); - } - while (s.heap_len >= 2); - - s.heap[--s.heap_max] = s.heap[1]; - - // At this point, the fields freq and dad are set. We can now - // generate the bit lengths. - - gen_bitlen(s); - - // The field len is now set, we can generate the bit codes - gen_codes(tree, max_code, s.bl_count); - } - - // Generate the codes for a given tree and bit counts (which need not be - // optimal). - // IN assertion: the array bl_count contains the bit length statistics for - // the given tree and the field len is set for all tree elements. - // OUT assertion: the field code is set for all tree elements of non - // zero code length. - internal static void gen_codes(short[] tree, int max_code, short[] bl_count) - { - short[] next_code = new short[MAX_BITS + 1]; // next code value for each bit length - short code = 0; // running code value - int bits; // bit index - int n; // code index - - // The distribution counts are first used to generate the code values - // without bit reversal. - for (bits = 1; bits <= MAX_BITS; bits++) - { - next_code[bits] = code = (short) ((code + bl_count[bits - 1]) << 1); - } - - // Check that the bit counts in bl_count are consistent. The last code - // must be all ones. - //Assert (code + bl_count[MAX_BITS]-1 == (1< 0); - return SupportClass.URShift(res, 1); - } - } -} \ No newline at end of file diff --git a/src/Zlib.net/ZInputStream.cs b/src/Zlib.net/ZInputStream.cs deleted file mode 100644 index ba04825..0000000 --- a/src/Zlib.net/ZInputStream.cs +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) 2006, ComponentAce -// http://www.componentace.com -// All rights reserved. - -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -// Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -// Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -// Neither the name of ComponentAce nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -/* -Copyright (c) 2001 Lapo Luchini. - -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. The names of the authors may not be used to endorse or promote products -derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 AUTHORS -OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -/* -* This program is based on zlib-1.1.3, so all credit should go authors -* Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) -* and contributors of zlib. -*/ -using System; -namespace ComponentAce.Compression.Libs.zlib -{ - - public class ZInputStream:System.IO.BinaryReader - { - internal void InitBlock() - { - flush = zlibConst.Z_NO_FLUSH; - buf = new byte[bufsize]; - } - virtual public int FlushMode - { - get - { - return (flush); - } - - set - { - this.flush = value; - } - - } - /// Returns the total number of bytes input so far. - virtual public long TotalIn - { - get - { - return z.total_in; - } - - } - /// Returns the total number of bytes output so far. - virtual public long TotalOut - { - get - { - return z.total_out; - } - - } - - protected ZStream z = new ZStream(); - protected int bufsize = 512; - protected int flush; - protected byte[] buf, buf1 = new byte[1]; - protected bool compress; - - internal System.IO.Stream in_Renamed = null; - - public ZInputStream(System.IO.Stream in_Renamed):base(in_Renamed) - { - InitBlock(); - this.in_Renamed = in_Renamed; - z.inflateInit(); - compress = false; - z.next_in = buf; - z.next_in_index = 0; - z.avail_in = 0; - } - - public ZInputStream(System.IO.Stream in_Renamed, int level):base(in_Renamed) - { - InitBlock(); - this.in_Renamed = in_Renamed; - z.deflateInit(level); - compress = true; - z.next_in = buf; - z.next_in_index = 0; - z.avail_in = 0; - } - - /*public int available() throws IOException { - return inf.finished() ? 0 : 1; - }*/ - - public override int Read() - { - if (read(buf1, 0, 1) == - 1) - return (- 1); - return (buf1[0] & 0xFF); - } - - internal bool nomoreinput = false; - - public int read(byte[] b, int off, int len) - { - if (len == 0) - return (0); - int err; - z.next_out = b; - z.next_out_index = off; - z.avail_out = len; - do - { - if ((z.avail_in == 0) && (!nomoreinput)) - { - // if buffer is empty and more input is avaiable, refill it - z.next_in_index = 0; - z.avail_in = SupportClass.ReadInput(in_Renamed, buf, 0, bufsize); //(bufsize Returns the total number of bytes input so far. - virtual public long TotalIn - { - get - { - return z.total_in; - } - - } - /// Returns the total number of bytes output so far. - virtual public long TotalOut - { - get - { - return z.total_out; - } - - } - - protected internal ZStream z = new ZStream(); - protected internal int bufsize = 4096; - protected internal int flush_Renamed_Field; - protected internal byte[] buf, buf1 = new byte[1]; - protected internal bool compress; - - private System.IO.Stream out_Renamed; - - public ZOutputStream(System.IO.Stream out_Renamed):base() - { - InitBlock(); - this.out_Renamed = out_Renamed; - z.inflateInit(); - compress = false; - } - - public ZOutputStream(System.IO.Stream out_Renamed, int level):base() - { - InitBlock(); - this.out_Renamed = out_Renamed; - z.deflateInit(level); - compress = true; - } - - public void WriteByte(int b) - { - buf1[0] = (byte) b; - Write(buf1, 0, 1); - } - //UPGRADE_TODO: The differences in the Expected value of parameters for method 'WriteByte' may cause compilation errors. 'ms-help://MS.VSCC.2003/commoner/redir/redirect.htm?keyword="jlca1092_3"' - public override void WriteByte(byte b) - { - WriteByte((int) b); - } - - public override void Write(System.Byte[] b1, int off, int len) - { - if (len == 0) - return ; - int err; - byte[] b = new byte[b1.Length]; - System.Array.Copy(b1, 0, b, 0, b1.Length); - z.next_in = b; - z.next_in_index = off; - z.avail_in = len; - do - { - z.next_out = buf; - z.next_out_index = 0; - z.avail_out = bufsize; - if (compress) - err = z.deflate(flush_Renamed_Field); - else - err = z.inflate(flush_Renamed_Field); - if (err != zlibConst.Z_OK && err != zlibConst.Z_STREAM_END) - throw new ZStreamException((compress?"de":"in") + "flating: " + z.msg); - out_Renamed.Write(buf, 0, bufsize - z.avail_out); - } - while (z.avail_in > 0 || z.avail_out == 0); - } - - public virtual void finish() - { - int err; - do - { - z.next_out = buf; - z.next_out_index = 0; - z.avail_out = bufsize; - if (compress) - { - err = z.deflate(zlibConst.Z_FINISH); - } - else - { - err = z.inflate(zlibConst.Z_FINISH); - } - if (err != zlibConst.Z_STREAM_END && err != zlibConst.Z_OK) - throw new ZStreamException((compress?"de":"in") + "flating: " + z.msg); - if (bufsize - z.avail_out > 0) - { - out_Renamed.Write(buf, 0, bufsize - z.avail_out); - } - } - while (z.avail_in > 0 || z.avail_out == 0); - try - { - Flush(); - } - catch - { - } - } - public virtual void end() - { - if (compress) - { - z.deflateEnd(); - } - else - { - z.inflateEnd(); - } - z.free(); - z = null; - } - public override void Close() - { - try - { - try - { - finish(); - } - catch - { - } - } - finally - { - end(); - out_Renamed.Close(); - out_Renamed = null; - } - } - - public override void Flush() - { - out_Renamed.Flush(); - } - //UPGRADE_TODO: The following method was automatically generated and it must be implemented in order to preserve the class logic. 'ms-help://MS.VSCC.2003/commoner/redir/redirect.htm?keyword="jlca1232_3"' - public override System.Int32 Read(System.Byte[] buffer, System.Int32 offset, System.Int32 count) - { - return 0; - } - //UPGRADE_TODO: The following method was automatically generated and it must be implemented in order to preserve the class logic. 'ms-help://MS.VSCC.2003/commoner/redir/redirect.htm?keyword="jlca1232_3"' - public override void SetLength(System.Int64 value) - { - } - //UPGRADE_TODO: The following method was automatically generated and it must be implemented in order to preserve the class logic. 'ms-help://MS.VSCC.2003/commoner/redir/redirect.htm?keyword="jlca1232_3"' - public override System.Int64 Seek(System.Int64 offset, System.IO.SeekOrigin origin) - { - return 0; - } - //UPGRADE_TODO: The following property was automatically generated and it must be implemented in order to preserve the class logic. 'ms-help://MS.VSCC.2003/commoner/redir/redirect.htm?keyword="jlca1232_3"' - public override System.Boolean CanRead - { - get - { - return false; - } - - } - //UPGRADE_TODO: The following property was automatically generated and it must be implemented in order to preserve the class logic. 'ms-help://MS.VSCC.2003/commoner/redir/redirect.htm?keyword="jlca1232_3"' - public override System.Boolean CanSeek - { - get - { - return false; - } - - } - //UPGRADE_TODO: The following property was automatically generated and it must be implemented in order to preserve the class logic. 'ms-help://MS.VSCC.2003/commoner/redir/redirect.htm?keyword="jlca1232_3"' - public override System.Boolean CanWrite - { - get - { - return false; - } - - } - //UPGRADE_TODO: The following property was automatically generated and it must be implemented in order to preserve the class logic. 'ms-help://MS.VSCC.2003/commoner/redir/redirect.htm?keyword="jlca1232_3"' - public override System.Int64 Length - { - get - { - return 0; - } - - } - //UPGRADE_TODO: The following property was automatically generated and it must be implemented in order to preserve the class logic. 'ms-help://MS.VSCC.2003/commoner/redir/redirect.htm?keyword="jlca1232_3"' - public override System.Int64 Position - { - get - { - return 0; - } - - set - { - } - - } - } -} \ No newline at end of file diff --git a/src/Zlib.net/ZStream.cs b/src/Zlib.net/ZStream.cs deleted file mode 100644 index 2601b44..0000000 --- a/src/Zlib.net/ZStream.cs +++ /dev/null @@ -1,233 +0,0 @@ -// Copyright (c) 2006, ComponentAce -// http://www.componentace.com -// All rights reserved. - -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -// Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -// Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -// Neither the name of ComponentAce nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -/* -Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -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. The names of the authors may not be used to endorse or promote products -derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 JCRAFT, -INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -/* -* This program is based on zlib-1.1.3, so all credit should go authors -* Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) -* and contributors of zlib. -*/ -using System; -namespace ComponentAce.Compression.Libs.zlib -{ - - sealed public class ZStream - { - - private const int MAX_WBITS = 15; // 32K LZ77 window - private static readonly int DEF_WBITS = MAX_WBITS; - - private const int Z_NO_FLUSH = 0; - private const int Z_PARTIAL_FLUSH = 1; - private const int Z_SYNC_FLUSH = 2; - private const int Z_FULL_FLUSH = 3; - private const int Z_FINISH = 4; - - private const int MAX_MEM_LEVEL = 9; - - private const int Z_OK = 0; - private const int Z_STREAM_END = 1; - private const int Z_NEED_DICT = 2; - private const int Z_ERRNO = - 1; - private const int Z_STREAM_ERROR = - 2; - private const int Z_DATA_ERROR = - 3; - private const int Z_MEM_ERROR = - 4; - private const int Z_BUF_ERROR = - 5; - private const int Z_VERSION_ERROR = - 6; - - public byte[] next_in; // next input byte - public int next_in_index; - public int avail_in; // number of bytes available at next_in - public long total_in; // total nb of input bytes read so far - - public byte[] next_out; // next output byte should be put there - public int next_out_index; - public int avail_out; // remaining free space at next_out - public long total_out; // total nb of bytes output so far - - public System.String msg; - - internal Deflate dstate; - internal Inflate istate; - - internal int data_type; // best guess about the data type: ascii or binary - - public long adler; - internal Adler32 _adler = new Adler32(); - - public int inflateInit() - { - return inflateInit(DEF_WBITS); - } - public int inflateInit(int w) - { - istate = new Inflate(); - return istate.inflateInit(this, w); - } - - public int inflate(int f) - { - if (istate == null) - return Z_STREAM_ERROR; - return istate.inflate(this, f); - } - public int inflateEnd() - { - if (istate == null) - return Z_STREAM_ERROR; - int ret = istate.inflateEnd(this); - istate = null; - return ret; - } - public int inflateSync() - { - if (istate == null) - return Z_STREAM_ERROR; - return istate.inflateSync(this); - } - public int inflateSetDictionary(byte[] dictionary, int dictLength) - { - if (istate == null) - return Z_STREAM_ERROR; - return istate.inflateSetDictionary(this, dictionary, dictLength); - } - - public int deflateInit(int level) - { - return deflateInit(level, MAX_WBITS); - } - public int deflateInit(int level, int bits) - { - dstate = new Deflate(); - return dstate.deflateInit(this, level, bits); - } - public int deflate(int flush) - { - if (dstate == null) - { - return Z_STREAM_ERROR; - } - return dstate.deflate(this, flush); - } - public int deflateEnd() - { - if (dstate == null) - return Z_STREAM_ERROR; - int ret = dstate.deflateEnd(); - dstate = null; - return ret; - } - public int deflateParams(int level, int strategy) - { - if (dstate == null) - return Z_STREAM_ERROR; - return dstate.deflateParams(this, level, strategy); - } - public int deflateSetDictionary(byte[] dictionary, int dictLength) - { - if (dstate == null) - return Z_STREAM_ERROR; - return dstate.deflateSetDictionary(this, dictionary, dictLength); - } - - // Flush as much pending output as possible. All deflate() output goes - // through this function so some applications may wish to modify it - // to avoid allocating a large strm->next_out buffer and copying into it. - // (See also read_buf()). - internal void flush_pending() - { - int len = dstate.pending; - - if (len > avail_out) - len = avail_out; - if (len == 0) - return ; - - if (dstate.pending_buf.Length <= dstate.pending_out || next_out.Length <= next_out_index || dstate.pending_buf.Length < (dstate.pending_out + len) || next_out.Length < (next_out_index + len)) - { - //System.Console.Out.WriteLine(dstate.pending_buf.Length + ", " + dstate.pending_out + ", " + next_out.Length + ", " + next_out_index + ", " + len); - //System.Console.Out.WriteLine("avail_out=" + avail_out); - } - - Array.Copy(dstate.pending_buf, dstate.pending_out, next_out, next_out_index, len); - - next_out_index += len; - dstate.pending_out += len; - total_out += len; - avail_out -= len; - dstate.pending -= len; - if (dstate.pending == 0) - { - dstate.pending_out = 0; - } - } - - // Read a new buffer from the current input stream, update the adler32 - // and total number of bytes read. All deflate() input goes through - // this function so some applications may wish to modify it to avoid - // allocating a large strm->next_in buffer and copying from it. - // (See also flush_pending()). - internal int read_buf(byte[] buf, int start, int size) - { - int len = avail_in; - - if (len > size) - len = size; - if (len == 0) - return 0; - - avail_in -= len; - - if (dstate.noheader == 0) - { - adler = _adler.adler32(adler, next_in, next_in_index, len); - } - Array.Copy(next_in, next_in_index, buf, start, len); - next_in_index += len; - total_in += len; - return len; - } - - public void free() - { - next_in = null; - next_out = null; - msg = null; - _adler = null; - } - } -} \ No newline at end of file diff --git a/src/Zlib.net/ZStreamException.cs b/src/Zlib.net/ZStreamException.cs deleted file mode 100644 index 062e237..0000000 --- a/src/Zlib.net/ZStreamException.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2006, ComponentAce -// http://www.componentace.com -// All rights reserved. - -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -// Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -// Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -// Neither the name of ComponentAce nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -/* -Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -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. The names of the authors may not be used to endorse or promote products -derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 JCRAFT, -INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -/* -* This program is based on zlib-1.1.3, so all credit should go authors -* Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) -* and contributors of zlib. -*/ -using System; -namespace ComponentAce.Compression.Libs.zlib -{ - - - public class ZStreamException:System.IO.IOException - { - public ZStreamException():base() - { - } - public ZStreamException(System.String s):base(s) - { - } - } -} \ No newline at end of file diff --git a/src/Zlib.net/Zlib.cs b/src/Zlib.net/Zlib.cs deleted file mode 100644 index 85f4f1d..0000000 --- a/src/Zlib.net/Zlib.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2006, ComponentAce -// http://www.componentace.com -// All rights reserved. - -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -// Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -// Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -// Neither the name of ComponentAce nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -/* -Copyright (c) 2000,2001,2002,2003 ymnk, JCraft,Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -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. The names of the authors may not be used to endorse or promote products -derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 JCRAFT, -INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -/* -* This program is based on zlib-1.1.3, so all credit should go authors -* Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu) -* and contributors of zlib. -*/ -using System; -namespace ComponentAce.Compression.Libs.zlib -{ - - sealed public class zlibConst - { - private const System.String version_Renamed_Field = "1.0.2"; - public static System.String version() - { - return version_Renamed_Field; - } - - // compression levels - public const int Z_NO_COMPRESSION = 0; - public const int Z_BEST_SPEED = 1; - public const int Z_BEST_COMPRESSION = 9; - public const int Z_DEFAULT_COMPRESSION = (- 1); - - // compression strategy - public const int Z_FILTERED = 1; - public const int Z_HUFFMAN_ONLY = 2; - public const int Z_DEFAULT_STRATEGY = 0; - - public const int Z_NO_FLUSH = 0; - public const int Z_PARTIAL_FLUSH = 1; - public const int Z_SYNC_FLUSH = 2; - public const int Z_FULL_FLUSH = 3; - public const int Z_FINISH = 4; - - public const int Z_OK = 0; - public const int Z_STREAM_END = 1; - public const int Z_NEED_DICT = 2; - public const int Z_ERRNO = - 1; - public const int Z_STREAM_ERROR = - 2; - public const int Z_DATA_ERROR = - 3; - public const int Z_MEM_ERROR = - 4; - public const int Z_BUF_ERROR = - 5; - public const int Z_VERSION_ERROR = - 6; - } -} \ No newline at end of file diff --git a/src/Zlib.net/Zlib.net.csproj b/src/Zlib.net/Zlib.net.csproj deleted file mode 100644 index 69ae647..0000000 --- a/src/Zlib.net/Zlib.net.csproj +++ /dev/null @@ -1,135 +0,0 @@ - - - - Local - 7.10.3077 - 2.0 - {0C89B7A2-A218-49E4-B545-5B044A45F977} - Debug - AnyCPU - - - zlib.net - - JScript - Grid - IE50 - false - Library - zlib - OnBuildSuccess - - - - - - 0.0 - v4.7.2 - - - - true - bin\x86\Debug\ - DEBUG;TRACE - 285212672 - 4096 - full - x86 - 7.3 - prompt - - - bin\x86\Release\ - TRACE - 285212672 - true - 4096 - x86 - 7.3 - prompt - - - true - bin\x64\Debug\ - DEBUG;TRACE - 285212672 - 4096 - full - x64 - 7.3 - prompt - - - bin\x64\Release\ - TRACE - 285212672 - true - 4096 - x64 - 7.3 - prompt - - - - System - - - System.Data - - - System.XML - - - - - Code - - - Code - - - Code - - - Code - - - Code - - - Code - - - Code - - - Code - - - Code - - - Code - - - Code - - - Code - - - Code - - - Code - - - Code - - - - - - - - \ No newline at end of file diff --git a/src/Zlib.net/history.txt b/src/Zlib.net/history.txt deleted file mode 100644 index 61a65b4..0000000 --- a/src/Zlib.net/history.txt +++ /dev/null @@ -1,29 +0,0 @@ -ZLIB.NET: History of changes -============================= - -version 1.04 (03/28/2007) - - - Problem with decompression some files is solved - - -version 1.03 (03/05/2007) - - - Problem with decomressing some files is solved - - -version 1.02 (01/29/2007) - - - Problem with decompressing large files using ZOutputStream is solved - - -version 1.01 (08/17/2006) - - - Demos are updated - - Some minor bugs are fixed - - -version 1.0 (07/06/2006) - - - -