Compare commits
	
		
			1 Commits
		
	
	
		
			master
			...
			experiment
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d8f73e21b4 | 
							
								
								
									
										12
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -5,7 +5,6 @@ build/
 | 
				
			|||||||
!**/src/test/**/build/
 | 
					!**/src/test/**/build/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### IntelliJ IDEA ###
 | 
					### IntelliJ IDEA ###
 | 
				
			||||||
.idea
 | 
					 | 
				
			||||||
.idea/modules.xml
 | 
					.idea/modules.xml
 | 
				
			||||||
.idea/jarRepositories.xml
 | 
					.idea/jarRepositories.xml
 | 
				
			||||||
.idea/compiler.xml
 | 
					.idea/compiler.xml
 | 
				
			||||||
@ -24,7 +23,8 @@ out/
 | 
				
			|||||||
.project
 | 
					.project
 | 
				
			||||||
.settings
 | 
					.settings
 | 
				
			||||||
.springBeans
 | 
					.springBeans
 | 
				
			||||||
.sts4-caches
 | 
					.sts4-cache
 | 
				
			||||||
 | 
					bin/
 | 
				
			||||||
!**/src/main/**/bin/
 | 
					!**/src/main/**/bin/
 | 
				
			||||||
!**/src/test/**/bin/
 | 
					!**/src/test/**/bin/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -39,10 +39,4 @@ out/
 | 
				
			|||||||
.vscode/
 | 
					.vscode/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Mac OS ###
 | 
					### Mac OS ###
 | 
				
			||||||
.DS_Store
 | 
					.DS_Store
 | 
				
			||||||
 | 
					 | 
				
			||||||
# More
 | 
					 | 
				
			||||||
.kotlin
 | 
					 | 
				
			||||||
/.idea/workspace.xml
 | 
					 | 
				
			||||||
/.gigaide/gigaide.properties
 | 
					 | 
				
			||||||
local.properties
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					# Default ignored files
 | 
				
			||||||
 | 
					/shelf/
 | 
				
			||||||
 | 
					/workspace.xml
 | 
				
			||||||
 | 
					# Editor-based HTTP Client requests
 | 
				
			||||||
 | 
					/httpRequests/
 | 
				
			||||||
 | 
					# Datasource local storage ignored files
 | 
				
			||||||
 | 
					/dataSources/
 | 
				
			||||||
 | 
					/dataSources.local.xml
 | 
				
			||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					<component name="ArtifactManager">
 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-js-0.2.4">
 | 
					  <artifact type="jar" name="kiloparsec-js-0.1.0-SNAPSHOT">
 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
				
			||||||
    <root id="archive" name="kiloparsec-js-0.2.4.jar">
 | 
					    <root id="archive" name="kiloparsec-js-0.1.0-SNAPSHOT.jar">
 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jsMain" />
 | 
					      <element id="module-output" name="kiloparsec.jsMain" />
 | 
				
			||||||
    </root>
 | 
					    </root>
 | 
				
			||||||
  </artifact>
 | 
					  </artifact>
 | 
				
			||||||
							
								
								
									
										6
									
								
								.idea/artifacts/kiloparsec_js_0_1_2_SNAPSHOT.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								.idea/artifacts/kiloparsec_js_0_1_2_SNAPSHOT.xml
									
									
									
										generated
									
									
									
								
							@ -1,6 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-js-0.1.2-SNAPSHOT">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-js-0.1.2-SNAPSHOT.jar" />
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_2_1_SNAPSHOT.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_2_1_SNAPSHOT.xml
									
									
									
										generated
									
									
									
								
							@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-js-0.2.1-SNAPSHOT">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-js-0.2.1-SNAPSHOT.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jsMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_2_2_SNAPSHOT.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_2_2_SNAPSHOT.xml
									
									
									
										generated
									
									
									
								
							@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-js-0.2.2-SNAPSHOT">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-js-0.2.2-SNAPSHOT.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jsMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_2_3.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_2_3.xml
									
									
									
										generated
									
									
									
								
							@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-js-0.2.3">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-js-0.2.3.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jsMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_2_5.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_2_5.xml
									
									
									
										generated
									
									
									
								
							@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-js-0.2.5">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-js-0.2.5.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jsMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_2_5_SNAPSHOT.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_2_5_SNAPSHOT.xml
									
									
									
										generated
									
									
									
								
							@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-js-0.2.5-SNAPSHOT">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-js-0.2.5-SNAPSHOT.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jsMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										6
									
								
								.idea/artifacts/kiloparsec_js_0_2_6.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								.idea/artifacts/kiloparsec_js_0_2_6.xml
									
									
									
										generated
									
									
									
								
							@ -1,6 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-js-0.2.6">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-js-0.2.6.jar" />
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_3_1.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_3_1.xml
									
									
									
										generated
									
									
									
								
							@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-js-0.3.1">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-js-0.3.1.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jsMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_3_1_SNAPSHOT.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_3_1_SNAPSHOT.xml
									
									
									
										generated
									
									
									
								
							@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-js-0.3.1-SNAPSHOT">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-js-0.3.1-SNAPSHOT.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jsMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_3_2.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_3_2.xml
									
									
									
										generated
									
									
									
								
							@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-js-0.3.2">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-js-0.3.2.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jsMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_3_3.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_3_3.xml
									
									
									
										generated
									
									
									
								
							@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-js-0.3.3">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-js-0.3.3.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jsMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_4_1.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_4_1.xml
									
									
									
										generated
									
									
									
								
							@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-js-0.4.1">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-js-0.4.1.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jsMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_4_3.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/artifacts/kiloparsec_js_0_4_3.xml
									
									
									
										generated
									
									
									
								
							@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-js-0.4.3">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-js-0.4.3.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jsMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					<component name="ArtifactManager">
 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-jvm-0.2.1-SNAPSHOT">
 | 
					  <artifact type="jar" name="kiloparsec-jvm-0.1.0-SNAPSHOT">
 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
				
			||||||
    <root id="archive" name="kiloparsec-jvm-0.2.1-SNAPSHOT.jar">
 | 
					    <root id="archive" name="kiloparsec-jvm-0.1.0-SNAPSHOT.jar">
 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jvmMain" />
 | 
					      <element id="module-output" name="kiloparsec.jvmMain" />
 | 
				
			||||||
    </root>
 | 
					    </root>
 | 
				
			||||||
  </artifact>
 | 
					  </artifact>
 | 
				
			||||||
@ -1,6 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-jvm-0.1.2-SNAPSHOT">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-jvm-0.1.2-SNAPSHOT.jar" />
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-jvm-0.2.2-SNAPSHOT">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-jvm-0.2.2-SNAPSHOT.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jvmMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/artifacts/kiloparsec_jvm_0_2_3.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/artifacts/kiloparsec_jvm_0_2_3.xml
									
									
									
										generated
									
									
									
								
							@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-jvm-0.2.3">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-jvm-0.2.3.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jvmMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/artifacts/kiloparsec_jvm_0_2_4.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/artifacts/kiloparsec_jvm_0_2_4.xml
									
									
									
										generated
									
									
									
								
							@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-jvm-0.2.4">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-jvm-0.2.4.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jvmMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/artifacts/kiloparsec_jvm_0_2_5.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/artifacts/kiloparsec_jvm_0_2_5.xml
									
									
									
										generated
									
									
									
								
							@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-jvm-0.2.5">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-jvm-0.2.5.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jvmMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-jvm-0.2.5-SNAPSHOT">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-jvm-0.2.5-SNAPSHOT.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jvmMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										6
									
								
								.idea/artifacts/kiloparsec_jvm_0_2_6.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								.idea/artifacts/kiloparsec_jvm_0_2_6.xml
									
									
									
										generated
									
									
									
								
							@ -1,6 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-jvm-0.2.6">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-jvm-0.2.6.jar" />
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/artifacts/kiloparsec_jvm_0_3_1.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/artifacts/kiloparsec_jvm_0_3_1.xml
									
									
									
										generated
									
									
									
								
							@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-jvm-0.3.1">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-jvm-0.3.1.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jvmMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-jvm-0.3.1-SNAPSHOT">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-jvm-0.3.1-SNAPSHOT.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jvmMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/artifacts/kiloparsec_jvm_0_3_2.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/artifacts/kiloparsec_jvm_0_3_2.xml
									
									
									
										generated
									
									
									
								
							@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-jvm-0.3.2">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-jvm-0.3.2.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jvmMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/artifacts/kiloparsec_jvm_0_3_3.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/artifacts/kiloparsec_jvm_0_3_3.xml
									
									
									
										generated
									
									
									
								
							@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-jvm-0.3.3">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-jvm-0.3.3.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jvmMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/artifacts/kiloparsec_jvm_0_4_1.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/artifacts/kiloparsec_jvm_0_4_1.xml
									
									
									
										generated
									
									
									
								
							@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-jvm-0.4.1">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-jvm-0.4.1.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jvmMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								.idea/artifacts/kiloparsec_jvm_0_4_3.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								.idea/artifacts/kiloparsec_jvm_0_4_3.xml
									
									
									
										generated
									
									
									
								
							@ -1,8 +0,0 @@
 | 
				
			|||||||
<component name="ArtifactManager">
 | 
					 | 
				
			||||||
  <artifact type="jar" name="kiloparsec-jvm-0.4.3">
 | 
					 | 
				
			||||||
    <output-path>$PROJECT_DIR$/build/libs</output-path>
 | 
					 | 
				
			||||||
    <root id="archive" name="kiloparsec-jvm-0.4.3.jar">
 | 
					 | 
				
			||||||
      <element id="module-output" name="kiloparsec.jvmMain" />
 | 
					 | 
				
			||||||
    </root>
 | 
					 | 
				
			||||||
  </artifact>
 | 
					 | 
				
			||||||
</component>
 | 
					 | 
				
			||||||
							
								
								
									
										24
									
								
								.idea/codeStyles/Project.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										24
									
								
								.idea/codeStyles/Project.xml
									
									
									
										generated
									
									
									
								
							@ -1,29 +1,5 @@
 | 
				
			|||||||
<component name="ProjectCodeStyleConfiguration">
 | 
					<component name="ProjectCodeStyleConfiguration">
 | 
				
			||||||
  <code_scheme name="Project" version="173">
 | 
					  <code_scheme name="Project" version="173">
 | 
				
			||||||
    <DBN-PSQL>
 | 
					 | 
				
			||||||
      <case-options enabled="true">
 | 
					 | 
				
			||||||
        <option name="KEYWORD_CASE" value="lower" />
 | 
					 | 
				
			||||||
        <option name="FUNCTION_CASE" value="lower" />
 | 
					 | 
				
			||||||
        <option name="PARAMETER_CASE" value="lower" />
 | 
					 | 
				
			||||||
        <option name="DATATYPE_CASE" value="lower" />
 | 
					 | 
				
			||||||
        <option name="OBJECT_CASE" value="preserve" />
 | 
					 | 
				
			||||||
      </case-options>
 | 
					 | 
				
			||||||
      <formatting-settings enabled="false" />
 | 
					 | 
				
			||||||
    </DBN-PSQL>
 | 
					 | 
				
			||||||
    <DBN-SQL>
 | 
					 | 
				
			||||||
      <case-options enabled="true">
 | 
					 | 
				
			||||||
        <option name="KEYWORD_CASE" value="lower" />
 | 
					 | 
				
			||||||
        <option name="FUNCTION_CASE" value="lower" />
 | 
					 | 
				
			||||||
        <option name="PARAMETER_CASE" value="lower" />
 | 
					 | 
				
			||||||
        <option name="DATATYPE_CASE" value="lower" />
 | 
					 | 
				
			||||||
        <option name="OBJECT_CASE" value="preserve" />
 | 
					 | 
				
			||||||
      </case-options>
 | 
					 | 
				
			||||||
      <formatting-settings enabled="false">
 | 
					 | 
				
			||||||
        <option name="STATEMENT_SPACING" value="one_line" />
 | 
					 | 
				
			||||||
        <option name="CLAUSE_CHOP_DOWN" value="chop_down_if_statement_long" />
 | 
					 | 
				
			||||||
        <option name="ITERATION_ELEMENTS_WRAPPING" value="chop_down_if_not_single" />
 | 
					 | 
				
			||||||
      </formatting-settings>
 | 
					 | 
				
			||||||
    </DBN-SQL>
 | 
					 | 
				
			||||||
    <ScalaCodeStyleSettings>
 | 
					    <ScalaCodeStyleSettings>
 | 
				
			||||||
      <option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" />
 | 
					      <option name="MULTILINE_STRING_CLOSING_QUOTES_ON_NEW_LINE" value="true" />
 | 
				
			||||||
    </ScalaCodeStyleSettings>
 | 
					    </ScalaCodeStyleSettings>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										6
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					<component name="InspectionProjectProfileManager">
 | 
				
			||||||
 | 
					  <profile version="1.0">
 | 
				
			||||||
 | 
					    <option name="myName" value="Project Default" />
 | 
				
			||||||
 | 
					    <inspection_tool class="ReplaceUntilWithRangeUntil" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
 | 
				
			||||||
 | 
					  </profile>
 | 
				
			||||||
 | 
					</component>
 | 
				
			||||||
							
								
								
									
										6
									
								
								.idea/kotlinc.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/kotlinc.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<project version="4">
 | 
				
			||||||
 | 
					  <component name="KotlinJpsPluginSettings">
 | 
				
			||||||
 | 
					    <option name="version" value="1.9.20" />
 | 
				
			||||||
 | 
					  </component>
 | 
				
			||||||
 | 
					</project>
 | 
				
			||||||
							
								
								
									
										6
									
								
								.idea/markdown.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								.idea/markdown.xml
									
									
									
										generated
									
									
									
								
							@ -1,6 +0,0 @@
 | 
				
			|||||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
					 | 
				
			||||||
<project version="4">
 | 
					 | 
				
			||||||
  <component name="MarkdownSettings">
 | 
					 | 
				
			||||||
    <option name="showProblemsInCodeBlocks" value="false" />
 | 
					 | 
				
			||||||
  </component>
 | 
					 | 
				
			||||||
</project>
 | 
					 | 
				
			||||||
							
								
								
									
										3
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
								
							@ -1,9 +1,10 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
<project version="4">
 | 
					<project version="4">
 | 
				
			||||||
  <component name="ExternalStorageConfigurationManager" enabled="true" />
 | 
					  <component name="ExternalStorageConfigurationManager" enabled="true" />
 | 
				
			||||||
  <component name="FrameworkDetectionExcludesConfiguration">
 | 
					  <component name="FrameworkDetectionExcludesConfiguration">
 | 
				
			||||||
    <file type="web" url="file://$PROJECT_DIR$" />
 | 
					    <file type="web" url="file://$PROJECT_DIR$" />
 | 
				
			||||||
  </component>
 | 
					  </component>
 | 
				
			||||||
  <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="corretto-17" project-jdk-type="JavaSDK">
 | 
					  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="17 (5)" project-jdk-type="JavaSDK">
 | 
				
			||||||
    <output url="file://$PROJECT_DIR$/out" />
 | 
					    <output url="file://$PROJECT_DIR$/out" />
 | 
				
			||||||
  </component>
 | 
					  </component>
 | 
				
			||||||
</project>
 | 
					</project>
 | 
				
			||||||
							
								
								
									
										6
									
								
								.idea/scala_compiler.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								.idea/scala_compiler.xml
									
									
									
										generated
									
									
									
								
							@ -1,6 +0,0 @@
 | 
				
			|||||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
					 | 
				
			||||||
<project version="4">
 | 
					 | 
				
			||||||
  <component name="ScalaCompilerConfiguration">
 | 
					 | 
				
			||||||
    <option name="separateProdTestSources" value="false" />
 | 
					 | 
				
			||||||
  </component>
 | 
					 | 
				
			||||||
</project>
 | 
					 | 
				
			||||||
							
								
								
									
										2
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
								
							@ -1,6 +1,6 @@
 | 
				
			|||||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
<project version="4">
 | 
					<project version="4">
 | 
				
			||||||
  <component name="VcsDirectoryMappings">
 | 
					  <component name="VcsDirectoryMappings">
 | 
				
			||||||
    <mapping directory="" vcs="Git" />
 | 
					    <mapping directory="$PROJECT_DIR$" vcs="Git" />
 | 
				
			||||||
  </component>
 | 
					  </component>
 | 
				
			||||||
</project>
 | 
					</project>
 | 
				
			||||||
							
								
								
									
										309
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										309
									
								
								README.md
									
									
									
									
									
								
							@ -1,309 +0,0 @@
 | 
				
			|||||||
# Kiloparsec
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
__Recommended version is `0.6.8`: to keep the code compatible with current and further versions we
 | 
					 | 
				
			||||||
ask to upgrade to `0.4.2` at least.__ Starting from this version some package names are changed for
 | 
					 | 
				
			||||||
better clarity and fast UDP endpoints are added.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
The new generation of __PARanoid SECurity__ protocol, advanced, faster, more secure. It also allows connecting any "
 | 
					 | 
				
			||||||
block device" transport to the same local interface. Out if the box it
 | 
					 | 
				
			||||||
provides the following transports:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
| name              | JVM  | JS  | wasmJS  | native |
 | 
					 | 
				
			||||||
|-------------------|:----:|:---:|:-------:|:------:|
 | 
					 | 
				
			||||||
| TCP/IP server     |  ✓   |     |         |   ✓    |
 | 
					 | 
				
			||||||
| TCP/IP client     |  ✓   |     |         |   ✓    |
 | 
					 | 
				
			||||||
| UDP server        |  ✓   |     |         |   ✓    |
 | 
					 | 
				
			||||||
| UDP client        |  ✓   |     |         |   ✓    |
 | 
					 | 
				
			||||||
| Websockets server |  ✓   |     |         |        |
 | 
					 | 
				
			||||||
| Websockets client |  ✓   |  ✓  |    ✓    |   ✓    |
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Note on version compatibility
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
We recommend using `0.6.12`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Since version 0.6.9 websocket protocol supports both text and binary frames; old clients are backward compatible with
 | 
					 | 
				
			||||||
mew servers, but new clients only can work with older servers only in default binary frame mode. Upgrade also your
 | 
					 | 
				
			||||||
servers to get better websocket compatibility[^1].
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Version 0.5.1 could be backward incompatible due to the upgrade of the crypto2.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Protocols >= 0.3.0 are not binary compatible with the previous version due to a more compact binary
 | 
					 | 
				
			||||||
format. The format from 0.3.0 onwards is supposed to keep compatible.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### ID calculation algorithm is changed since 0.4.1
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
We recommend to upgrade to 0.4+ ASAP as public/shared key id derivation method was changed for even higher security.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Supported native targets
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- iosArm64, iosX64
 | 
					 | 
				
			||||||
- macosArm64, macosArm64
 | 
					 | 
				
			||||||
- linuxArm64, linuxX64
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Non-native targets
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- JS (browser and Node.js)
 | 
					 | 
				
			||||||
- JVM (android, macOS, windows, linux, everywhere where JRE is installed)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## TCP/IP and UDP transports
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
These are the fastest based on async socket implementation of a ktor client. They work everywhere but JS target as
 | 
					 | 
				
			||||||
there are currently no widely adopted sockets for browser JavaScript.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
While UDP is faster than TCP/IP, it is less reliable, especially with commands and return values that serialize to more
 | 
					 | 
				
			||||||
than 240 bytes approx, and has no retransmission facilities (use TCP!). UDP, though, shines when all you need is
 | 
					 | 
				
			||||||
to [push](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-remote-interface/push.html) with
 | 
					 | 
				
			||||||
little or no data in it.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Websockets server
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
While it is much slower than TCP or UDP, it is still faster than any http-based API; it uses binary frames based on
 | 
					 | 
				
			||||||
the Ktor server framework to easily integrate with web services. We recommend using it instead of a classic HTTP API as
 | 
					 | 
				
			||||||
it beats it in terms of speed and server load even with HTTP/2.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
We recommend to create the `KiloInterface<S>` instance and connect it to the websockets and tcp servers in real
 | 
					 | 
				
			||||||
applications to get easy access from anywhere.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Websocket client
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
It is slower than TCP or UDP, but it works on literally all platforms. See the sample below.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Usage
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
The library should be used as a maven dependency, not as source.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Adding dependency
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Declare maven repository:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Add the private repository to your `build.gradle.kts`, like:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```kotlin
 | 
					 | 
				
			||||||
repositories {
 | 
					 | 
				
			||||||
    maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Add dependency block
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
It could be, depending on your project structure, something like:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```kotlin
 | 
					 | 
				
			||||||
val commonMain by getting {
 | 
					 | 
				
			||||||
    dependencies {
 | 
					 | 
				
			||||||
        api("net.sergeych:kiloparsec:0.6.8")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Create a shared interface for your server and the client
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
It could be a multiplatform library that exports it or just a shared or copied source file declaring structures
 | 
					 | 
				
			||||||
and functions available, like:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```kotlin
 | 
					 | 
				
			||||||
// Api.kt
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@Serializable
 | 
					 | 
				
			||||||
class FooArgs(val text: String, val number: Int = 42)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Server-side interface
 | 
					 | 
				
			||||||
val cmdSetFoo by command<FooArgs, Unit>()
 | 
					 | 
				
			||||||
val cmdGetFoo by command<Unit, FooArgs>()
 | 
					 | 
				
			||||||
val cmdPing by command<String, String>()
 | 
					 | 
				
			||||||
val cmdCheckConnected by command<Unit, Boolean>()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// client-side interface (called from the server)
 | 
					 | 
				
			||||||
val cmdPushClient by command<String, Unit>()
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Call it from the client:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Remember, we need to implement client interface `cmdPushClient` in our example, so we need to provide
 | 
					 | 
				
			||||||
local interface too:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```kotlin
 | 
					 | 
				
			||||||
// Unit: no session on the client:
 | 
					 | 
				
			||||||
val client = websocketClient<Unit>("wss://your.host.com/kp") {
 | 
					 | 
				
			||||||
    // This is server-callable function we export:
 | 
					 | 
				
			||||||
    on(cmdPushClient) {
 | 
					 | 
				
			||||||
        "server push: $it"
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// If we want to collect connected state changes (this is optional)
 | 
					 | 
				
			||||||
launch {
 | 
					 | 
				
			||||||
    client.connectedStateFlow.collect {
 | 
					 | 
				
			||||||
        if (it)
 | 
					 | 
				
			||||||
            println("I am connected")
 | 
					 | 
				
			||||||
        else
 | 
					 | 
				
			||||||
            println("trying to connect...")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// now we can call server's functions
 | 
					 | 
				
			||||||
client.call(cmdSetFoo, FooArgs("bar", 117))
 | 
					 | 
				
			||||||
assertEquals(FooArgs("bar", 117), client.call(cmdGetFoo))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Create a ktor-based server
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Normally the server side needs some session. It is convenient and avoids sending repeating data on each request speeding
 | 
					 | 
				
			||||||
up
 | 
					 | 
				
			||||||
the protocol. With KILOPARSEC, it is a rather basic operation:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
~~~kotlin
 | 
					 | 
				
			||||||
// Our session just keeps Foo for cmd{Get|Set}Foo:
 | 
					 | 
				
			||||||
data class Session(var fooState: FooArgs? = null)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Let's now provide interface we export, it will be used on each connection automatically:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Note server interface uses Session:
 | 
					 | 
				
			||||||
val serverInterface = KiloInterface<Session>().apply {
 | 
					 | 
				
			||||||
    onConnected {
 | 
					 | 
				
			||||||
        // Do some initialization
 | 
					 | 
				
			||||||
        session.fooState = null
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    // Exceptions are passed through the network and re-created (re-thrown) on other side:
 | 
					 | 
				
			||||||
    on(cmdGetFoo) { session.fooState ?: throw IllegalStateException("foo is not yet set") }
 | 
					 | 
				
			||||||
    on(cmdSetFoo) { session.fooState = it }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// now create server using ktor (see ktor project for more):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
val ns: NettyApplicationEngine = embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = {
 | 
					 | 
				
			||||||
    setupWebsocketServer(serverInterface) { Session() }
 | 
					 | 
				
			||||||
}).start(wait = false)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
~~~
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Create a TCP / IP client and server
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Using plain TCP/IP is even simpler, and it works way faster than websocket one, and is _the same
 | 
					 | 
				
			||||||
protected as `wss://` (and `ws://`) the variant above due to same kiloparsec encryption in both cases. Still, a TCP/IP
 | 
					 | 
				
			||||||
client is not available in JavaScript browser targets, and custom TCP ports could often be blocked by firewalls.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Documentation is available in samples here:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- [TCP/IP server creation](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-server/index.html)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- [TCP/IP client](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-client/index.html)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
In short, there are two functions that implement asynchronous TCP/IP transport on all platforms buy JS:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- [acceptTcpDevice](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec.adapter/accept-tcp-device.html?query=fun%20acceptTcpDevice(port:%20Int):%20Flow%3CInetTransportDevice%3E)
 | 
					 | 
				
			||||||
  to create a server
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- [connectTcpDevice](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec.adapter/connect-tcp-device.html)
 | 
					 | 
				
			||||||
  to connect to the server
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## UDP client and server
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Is very much straightforward, same as with TCP/IP:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- [UDP server creation](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec.adapter/accept-udp-device.html)
 | 
					 | 
				
			||||||
- [Connect UDP client](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec.adapter/connect-udp-device.html)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### UDP specifics
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Command size
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Each command invocation and result are packed in a separate UDP diagram using effective binary packing.
 | 
					 | 
				
			||||||
Thus, for the best results commands and results should be relatively short, best to fit into 240 bytes. While bigger
 | 
					 | 
				
			||||||
datagrams are often transmitted successfully, the probability of the effective transmission drops with the size.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Kiloparsec UDP transport does not retransmit not delivered packets. Use TCP/IP or websocket if it is a concern.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
For the best results, we recommend
 | 
					 | 
				
			||||||
using [push](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-remote-interface/index.html#1558240250%2FFunctions%2F788909594)
 | 
					 | 
				
			||||||
for remote interfaces with UDP.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#### Timeouts
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
As Datagrams do not form protocol itself, kiloparsec issues pings when no data is circulated between parties.
 | 
					 | 
				
			||||||
When no pings are received long enough, the kiloparsec connection is closed. There are `maxInactivityTimeout` in all
 | 
					 | 
				
			||||||
relevant functions and constructors.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Client should not issue pings manually.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Reusing code between servers
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
The same instance of
 | 
					 | 
				
			||||||
the [KiloInterface](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-interface/index.html?query=open%20class%20KiloInterface%3CS%3E%20:%20LocalInterface%3CKiloScope%3CS%3E%3E)
 | 
					 | 
				
			||||||
could easily be reused with all instances of servers with different protocols.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
This is a common practice to create a business logic in a `KiloInterface`, then create a TCP/IP and Websocket servers
 | 
					 | 
				
			||||||
passing the same instance of the logic to both.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Note on the server identification
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
We do not recommend relying on TLS (HTTPS://, WSS://) host identification solely; in the modern world there is
 | 
					 | 
				
			||||||
a high probability of attacks on unfriendly (in respect to at least some of your users) states to the SSL certificates
 | 
					 | 
				
			||||||
chain, in which case the [MITM attack] and spoofing will be undetected. Check
 | 
					 | 
				
			||||||
the [remoteId](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-client/remote-id.html?query=suspend%20fun%20remoteId():%20VerifyingPublicKey?)
 | 
					 | 
				
			||||||
in your client on each connection and provide the
 | 
					 | 
				
			||||||
safe [serverSecretKey](https://code.sergeych.net/docs/kiloparsec/kiloparsec/net.sergeych.kiloparsec/-kilo-server/index.html)
 | 
					 | 
				
			||||||
when creating a server.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
This will effectively protect against certificate chain spoofing in the case of the application installed from the
 | 
					 | 
				
			||||||
trusted source.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
__Important note__. The web application could not be completely secured this way unless is loaded from the IP-address,
 | 
					 | 
				
			||||||
as the DNS could be spoofed the same, especially when used with `Cloudflare` or other CDN that can transparently
 | 
					 | 
				
			||||||
substitute the whole site. For applications, we strongly recommend not using CDN except your own, controlled ones. You
 | 
					 | 
				
			||||||
generally can't neither detect nor repel [MITM attack] performed from _any single cloudflare 'ray'_.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## See also:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- [Source documentation](https://code.sergeych.net/docs/kiloparsec/)
 | 
					 | 
				
			||||||
- [Project's WIKI](https://gitea.sergeych.net/SergeychWorks/kiloparsec/wiki)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Details
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
It is not compatible with Parsec family and no more based on the Universa crypto library. To better fit
 | 
					 | 
				
			||||||
the modern state of threats and rate of cyber crimes, KiloParsec uses more encryption and random key exchange on each
 | 
					 | 
				
			||||||
and every connection (while parsec caches session keys to avoid time-consuming keys exchange). For the same reason,
 | 
					 | 
				
			||||||
keys cryptography for session is shifted to use ed25519 curves, which are supposed to provide agreeable strength with
 | 
					 | 
				
			||||||
enough speed to protect every connection with unique new keys. Also, we completely get rid of SHA2.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Kiloparsec also uses a denser binary format [bipack](https://gitea.sergeych.net/SergeychWorks/mp_bintools), no more
 | 
					 | 
				
			||||||
key-values,
 | 
					 | 
				
			||||||
which reveals much less on the inner data structure, providing advanced
 | 
					 | 
				
			||||||
typed RPC interfaces with kotlinx.serialization. There is also Rust
 | 
					 | 
				
			||||||
implementation [bipack_ru](https://gitea.sergeych.net/DiWAN/bipack_ru).
 | 
					 | 
				
			||||||
The architecture allows connecting the same functional interfaces to several various type channels at once.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Also, the difference from parsecs is that there are no more unencrypted layer commands available to users.
 | 
					 | 
				
			||||||
All RPC is performed over the encrypted connection.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Technical description
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Kiloparsec is a full-duplex fully async (coroutine-based) Remote Procedure Call protocol with typed parameters
 | 
					 | 
				
			||||||
and support for serializing exceptions (e.g. exception thrown while executing remote command will be caught and
 | 
					 | 
				
			||||||
rethrown at the caller context).
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Kiloparsec is not REST, it _has advanced session mechanisms_ and built-in authentication based on the same curve keys.
 | 
					 | 
				
			||||||
Integrated tools to prevent MITM attacks include also non-transferred independently generated token that is calculated
 | 
					 | 
				
			||||||
independently on the ends and is never transferred with the network. Comparing it somehow (visually, with QR code, etc.)
 | 
					 | 
				
			||||||
could add a very robust guarantee of the connection safety and ingenuity.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Kiloparsec has a built-in completely asynchronous (coroutine-based top-down) transport layer based on TCP (JVM only as
 | 
					 | 
				
			||||||
for
 | 
					 | 
				
			||||||
now) and the same async Websocket-based transport based on KTOR. Websocket client is multiplatform, though the server is
 | 
					 | 
				
			||||||
JVM only insofar.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Licensing
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
This is a work in progress, not yet moved to the public domain;
 | 
					 | 
				
			||||||
you need to obtain a license from https://8-rays.dev or [Sergey Chernov]. For open source projects it will most be free
 | 
					 | 
				
			||||||
on some special terms.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
It will be moved to open source; we also guarantee that it will be moved to open source immediately if the software
 | 
					 | 
				
			||||||
export restrictions are lifted. We do not support such practices here at 8-rays.dev.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[MITM]: https://en.wikipedia.org/wiki/Man-in-the-middle_attack
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[Sergey Chernov]: https://t.me/real_sergeych
 | 
					 | 
				
			||||||
[^1]: On some new Xiaomi phones we found problems with websocket binary frames, probably in ktor; use text frames
 | 
					 | 
				
			||||||
otherwise. 
 | 
					 | 
				
			||||||
							
								
								
									
										14
									
								
								bin/pubdocs
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								bin/pubdocs
									
									
									
									
									
								
							@ -1,14 +0,0 @@
 | 
				
			|||||||
#!/bin/bash
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
# Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
#  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
# terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
# To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
# real dot sergeych at gmail.
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
set -e
 | 
					 | 
				
			||||||
./gradlew dokkaHtml
 | 
					 | 
				
			||||||
rsync -avz ./build/dokka/* code.sergeych.net:/bigstore/sergeych_pub/code/docs/kiloparsec
 | 
					 | 
				
			||||||
							
								
								
									
										194
									
								
								build.gradle.kts
									
									
									
									
									
								
							
							
						
						
									
										194
									
								
								build.gradle.kts
									
									
									
									
									
								
							@ -1,60 +1,51 @@
 | 
				
			|||||||
/*
 | 
					import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType
 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
plugins {
 | 
					plugins {
 | 
				
			||||||
    kotlin("multiplatform") version "2.2.20"
 | 
					    kotlin("multiplatform") version "1.9.20"
 | 
				
			||||||
    id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
 | 
					    id("org.jetbrains.kotlin.plugin.serialization") version "1.9.20"
 | 
				
			||||||
    id("com.android.library") version "8.5.2" apply true
 | 
					 | 
				
			||||||
    `maven-publish`
 | 
					    `maven-publish`
 | 
				
			||||||
    id("org.jetbrains.dokka") version "1.9.20"
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
group = "net.sergeych"
 | 
					group = "net.sergeych"
 | 
				
			||||||
version = "0.6.12"
 | 
					version = "0.1.0-SNAPSHOT"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
repositories {
 | 
					repositories {
 | 
				
			||||||
    mavenCentral()
 | 
					    mavenCentral()
 | 
				
			||||||
    mavenLocal()
 | 
					    mavenLocal()
 | 
				
			||||||
    maven("https://maven.universablockchain.com/")
 | 
					    maven("https://maven.universablockchain.com/")
 | 
				
			||||||
    maven("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
 | 
					 | 
				
			||||||
    maven("https://gitea.sergeych.net/api/packages/YoungBlood/maven")
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
kotlin {
 | 
					kotlin {
 | 
				
			||||||
    jvmToolchain(17)
 | 
					    jvm {
 | 
				
			||||||
    jvm()
 | 
					        jvmToolchain(8)
 | 
				
			||||||
    js {
 | 
					        withJava()
 | 
				
			||||||
        browser {
 | 
					        testRuns.named("test") {
 | 
				
			||||||
 | 
					            executionTask.configure {
 | 
				
			||||||
 | 
					                useJUnitPlatform()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        nodejs()
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    linuxX64()
 | 
					    js(KotlinJsCompilerType.IR) {
 | 
				
			||||||
    linuxArm64()
 | 
					        browser {
 | 
				
			||||||
 | 
					//            commonWebpackConfig {
 | 
				
			||||||
    macosArm64()
 | 
					//                cssSupport {
 | 
				
			||||||
    macosX64()
 | 
					//                    enabled.set(true)
 | 
				
			||||||
    iosX64()
 | 
					//                }
 | 
				
			||||||
    iosArm64()
 | 
					//            }
 | 
				
			||||||
    iosSimulatorArm64()
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    mingwX64()
 | 
					    val hostOs = System.getProperty("os.name")
 | 
				
			||||||
 | 
					    val isArm64 = System.getProperty("os.arch") == "aarch64"
 | 
				
			||||||
    androidTarget()
 | 
					    val isMingwX64 = hostOs.startsWith("Windows")
 | 
				
			||||||
    @OptIn(ExperimentalWasmDsl::class)
 | 
					    val nativeTarget = when {
 | 
				
			||||||
    wasmJs {
 | 
					        hostOs == "Mac OS X" && isArm64 -> macosArm64("native")
 | 
				
			||||||
        browser()
 | 
					        hostOs == "Mac OS X" && !isArm64 -> macosX64("native")
 | 
				
			||||||
 | 
					        hostOs == "Linux" && isArm64 -> linuxArm64("native")
 | 
				
			||||||
 | 
					        hostOs == "Linux" && !isArm64 -> linuxX64("native")
 | 
				
			||||||
 | 
					        isMingwX64 -> mingwX64("native")
 | 
				
			||||||
 | 
					        else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val ktor_version = "3.1.1"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sourceSets {
 | 
					    sourceSets {
 | 
				
			||||||
        all {
 | 
					        all {
 | 
				
			||||||
@ -65,125 +56,28 @@ kotlin {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        val commonMain by getting {
 | 
					        val commonMain by getting {
 | 
				
			||||||
            dependencies {
 | 
					            dependencies {
 | 
				
			||||||
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
 | 
					                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
 | 
				
			||||||
                implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
 | 
					                implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
 | 
				
			||||||
                api("io.ktor:ktor-client-core:$ktor_version")
 | 
					
 | 
				
			||||||
                api("net.sergeych:crypto2:0.8.5")
 | 
					                implementation("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.0")
 | 
				
			||||||
            }
 | 
					                api("com.ionspin.kotlin:bignum:0.3.8")
 | 
				
			||||||
        }
 | 
					
 | 
				
			||||||
        val androidMain by getting {
 | 
					                api("net.sergeych:mp_bintools:0.0.6-SNAPSHOT")
 | 
				
			||||||
            dependencies {
 | 
					                api("net.sergeych:mp_stools:1.4.1")
 | 
				
			||||||
                implementation("io.ktor:ktor-client-okhttp:$ktor_version")
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        val ktorSocketMain by creating {
 | 
					 | 
				
			||||||
            dependsOn(commonMain)
 | 
					 | 
				
			||||||
            dependencies {
 | 
					 | 
				
			||||||
                implementation("io.ktor:ktor-network:$ktor_version")
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        val commonTest by getting {
 | 
					        val commonTest by getting {
 | 
				
			||||||
            dependencies {
 | 
					            dependencies {
 | 
				
			||||||
                implementation(kotlin("test"))
 | 
					                implementation(kotlin("test"))
 | 
				
			||||||
                implementation("org.slf4j:slf4j-simple:2.0.9")
 | 
					                implementation("org.slf4j:slf4j-simple:2.0.9")
 | 
				
			||||||
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1")
 | 
					                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        val ktorSocketTest by creating {
 | 
					 | 
				
			||||||
            dependsOn(commonTest)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        val jvmMain by getting {
 | 
					 | 
				
			||||||
            dependencies {
 | 
					 | 
				
			||||||
                implementation("io.ktor:ktor-server-core:$ktor_version")
 | 
					 | 
				
			||||||
                implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
 | 
					 | 
				
			||||||
                implementation("io.ktor:ktor-server-websockets-jvm:$ktor_version")
 | 
					 | 
				
			||||||
                implementation("io.ktor:ktor-server-netty:$ktor_version")
 | 
					 | 
				
			||||||
                api("io.ktor:ktor-client-cio:$ktor_version")
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            dependsOn(ktorSocketMain)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        val jvmTest by getting {
 | 
					 | 
				
			||||||
            dependsOn(ktorSocketTest)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        val jsMain by getting {
 | 
					 | 
				
			||||||
            dependencies {
 | 
					 | 
				
			||||||
                implementation("io.ktor:ktor-client-js:$ktor_version")
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        val jvmMain by getting
 | 
				
			||||||
 | 
					        val jvmTest by getting
 | 
				
			||||||
 | 
					        val jsMain by getting
 | 
				
			||||||
        val jsTest by getting
 | 
					        val jsTest by getting
 | 
				
			||||||
        val macosArm64Main by getting {
 | 
					        val nativeMain by getting
 | 
				
			||||||
            dependsOn(ktorSocketMain)
 | 
					        val nativeTest by getting
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        val macosArm64Test by getting {
 | 
					 | 
				
			||||||
            dependsOn(ktorSocketTest)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        val macosX64Main by getting {
 | 
					 | 
				
			||||||
            dependsOn(ktorSocketMain)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        val iosX64Main by getting {
 | 
					 | 
				
			||||||
            dependsOn(ktorSocketMain)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        val iosX64Test by getting {
 | 
					 | 
				
			||||||
            dependsOn(ktorSocketTest)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        val iosArm64Main by getting {
 | 
					 | 
				
			||||||
            dependsOn(ktorSocketMain)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        val iosArm64Test by getting {
 | 
					 | 
				
			||||||
            dependsOn(ktorSocketTest)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        val linuxArm64Main by getting {
 | 
					 | 
				
			||||||
            dependsOn(ktorSocketMain)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        val linuxArm64Test by getting {
 | 
					 | 
				
			||||||
            dependsOn(ktorSocketTest)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        val linuxX64Main by getting {
 | 
					 | 
				
			||||||
            dependsOn(ktorSocketMain)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        val linuxX64Test by getting {
 | 
					 | 
				
			||||||
            dependsOn(ktorSocketTest)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
publishing {
 | 
					 | 
				
			||||||
    val mavenToken by lazy {
 | 
					 | 
				
			||||||
        File("${System.getProperty("user.home")}/.gitea_token").readText()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    repositories {
 | 
					 | 
				
			||||||
        maven {
 | 
					 | 
				
			||||||
            credentials(HttpHeaderCredentials::class) {
 | 
					 | 
				
			||||||
                name = "Authorization"
 | 
					 | 
				
			||||||
                value = mavenToken
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            url = uri("https://gitea.sergeych.net/api/packages/SergeychWorks/maven")
 | 
					 | 
				
			||||||
            authentication {
 | 
					 | 
				
			||||||
                create("Authorization", HttpHeaderAuthentication::class)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
tasks.dokkaHtml.configure {
 | 
					 | 
				
			||||||
    outputDirectory.set(buildDir.resolve("dokka"))
 | 
					 | 
				
			||||||
    dokkaSourceSets {
 | 
					 | 
				
			||||||
        configureEach {
 | 
					 | 
				
			||||||
//            includes.from("docs/bipack.md")
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
android {
 | 
					 | 
				
			||||||
    namespace = "net.sergeych.kiloparsec"
 | 
					 | 
				
			||||||
    compileSdk = 34
 | 
					 | 
				
			||||||
    defaultConfig {
 | 
					 | 
				
			||||||
        minSdk = 24
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    compileOptions {
 | 
					 | 
				
			||||||
        sourceCompatibility = JavaVersion.VERSION_17
 | 
					 | 
				
			||||||
        targetCompatibility = JavaVersion.VERSION_17
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,15 +1 @@
 | 
				
			|||||||
#
 | 
					 | 
				
			||||||
# Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
#  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
# terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
# To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
# real dot sergeych at gmail.
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
kotlin.code.style=official
 | 
					kotlin.code.style=official
 | 
				
			||||||
kotlin.mpp.applyDefaultHierarchyTemplate=false
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
kotlin.daemon.jvmargs=-Xmx2048m
 | 
					 | 
				
			||||||
kotlin.native.ignoreDisabledTargets=true
 | 
					 | 
				
			||||||
							
								
								
									
										12
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							@ -1,15 +1,5 @@
 | 
				
			|||||||
#
 | 
					 | 
				
			||||||
# Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
#  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
# terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
# To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
# real dot sergeych at gmail.
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
distributionBase=GRADLE_USER_HOME
 | 
					distributionBase=GRADLE_USER_HOME
 | 
				
			||||||
distributionPath=wrapper/dists
 | 
					distributionPath=wrapper/dists
 | 
				
			||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
 | 
					distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
 | 
				
			||||||
zipStoreBase=GRADLE_USER_HOME
 | 
					zipStoreBase=GRADLE_USER_HOME
 | 
				
			||||||
zipStorePath=wrapper/dists
 | 
					zipStorePath=wrapper/dists
 | 
				
			||||||
							
								
								
									
										16
									
								
								gradlew
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								gradlew
									
									
									
									
										vendored
									
									
								
							@ -1,13 +1,19 @@
 | 
				
			|||||||
#!/bin/sh
 | 
					#!/bin/sh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					# Copyright © 2015-2021 the original authors.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  You may use, distribute and modify this code under the
 | 
					# Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
# terms of the private license, which you must obtain from the author
 | 
					# you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					# You may obtain a copy of the License at
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					#      https://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
# real dot sergeych at gmail.
 | 
					#
 | 
				
			||||||
 | 
					# Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					# distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					# See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					# limitations under the License.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
 | 
					
 | 
				
			||||||
##############################################################################
 | 
					##############################################################################
 | 
				
			||||||
 | 
				
			|||||||
@ -1,16 +1,5 @@
 | 
				
			|||||||
    /*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pluginManagement {
 | 
					pluginManagement {
 | 
				
			||||||
    repositories {
 | 
					    repositories {
 | 
				
			||||||
        google()
 | 
					 | 
				
			||||||
        mavenCentral()
 | 
					        mavenCentral()
 | 
				
			||||||
        gradlePluginPortal()
 | 
					        gradlePluginPortal()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										27
									
								
								src/commonMain/kotlin/net/sergeych/crypto/InitCrypto.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/commonMain/kotlin/net/sergeych/crypto/InitCrypto.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					package net.sergeych.crypto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.ionspin.kotlin.crypto.LibsodiumInitializer
 | 
				
			||||||
 | 
					import kotlinx.coroutines.sync.Mutex
 | 
				
			||||||
 | 
					import kotlinx.coroutines.sync.withLock
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private var isReady = false
 | 
				
			||||||
 | 
					private val readyAccess = Mutex()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Library initialization: should be called before all other calls.
 | 
				
			||||||
 | 
					 * It is safe and with little performance penalty to call it multiple times.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					suspend fun initCrypto() {
 | 
				
			||||||
 | 
					    // faster to check with no lock
 | 
				
			||||||
 | 
					    if( !isReady) {
 | 
				
			||||||
 | 
					        readyAccess.withLock {
 | 
				
			||||||
 | 
					            // recheck with lock, it could be ready by now
 | 
				
			||||||
 | 
					            if( !isReady ) {
 | 
				
			||||||
 | 
					                LibsodiumInitializer.initialize()
 | 
				
			||||||
 | 
					                isReady = true
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										81
									
								
								src/commonMain/kotlin/net/sergeych/crypto/SignedBox.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/commonMain/kotlin/net/sergeych/crypto/SignedBox.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,81 @@
 | 
				
			|||||||
 | 
					package net.sergeych.crypto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import kotlinx.serialization.Serializable
 | 
				
			||||||
 | 
					import kotlinx.serialization.Transient
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Multi-signed data box. Use [SignedBox.invoke] to easily create
 | 
				
			||||||
 | 
					 * instances and [SignedBox.plus] to add more signatures (signing keys), and
 | 
				
			||||||
 | 
					 * [SignedBox.contains] to check for a specific key signature presence.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * It is serializable and checks integrity on deserialization. If any of seals does not
 | 
				
			||||||
 | 
					 * match the signed [message], it throws [IllegalSignatureException] _on deserialization_.
 | 
				
			||||||
 | 
					 * E.g., if you have it deserialized, it is ok, check it contains all needed keys among
 | 
				
			||||||
 | 
					 * signers.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * __The main constructor is used for deserializing only__. Don't use it directly unless you
 | 
				
			||||||
 | 
					 * know what you are doing as it may be dangerous.Use one of the above to create or change it.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class SignedBox(
 | 
				
			||||||
 | 
					    val message: UByteArray,
 | 
				
			||||||
 | 
					    private val seals: List<Seal>,
 | 
				
			||||||
 | 
					    @Transient
 | 
				
			||||||
 | 
					    private val checkOnInit: Boolean = true
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * If this instance is not signed by a given key, return new instance signed also by this
 | 
				
			||||||
 | 
					     * key, or return unchanged (same) object if it is already signed by this key; you
 | 
				
			||||||
 | 
					     * _can't assume it always returns a copied object!_
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    operator fun plus(key: Key.Signing): SignedBox =
 | 
				
			||||||
 | 
					        if (key.verifying in this) this
 | 
				
			||||||
 | 
					        else SignedBox(message, seals + Seal.create(key, message), false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Check that it is signed with a specified key.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    operator fun contains(verifyingKey: Key.Verifying): Boolean {
 | 
				
			||||||
 | 
					        return seals.any { it.key == verifyingKey }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    init {
 | 
				
			||||||
 | 
					        if (seals.isEmpty()) throw IllegalArgumentException("there should be at least one seal")
 | 
				
			||||||
 | 
					        if (checkOnInit) {
 | 
				
			||||||
 | 
					            if (!seals.all { it.verify(message) }) throw IllegalSignatureException()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * A key + signature pair for [SignedBox] boxes, usually you don't use it
 | 
				
			||||||
 | 
					     * directly bug call [SignedBox] constructor or [SignedBox.plus] to
 | 
				
			||||||
 | 
					     * add seals.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    @Serializable
 | 
				
			||||||
 | 
					    data class Seal(val key: Key.Verifying, val signature: UByteArray) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fun verify(message: UByteArray): Boolean = key.verify(signature, message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        companion object {
 | 
				
			||||||
 | 
					            fun create(key: Key.Signing, message: UByteArray): Seal {
 | 
				
			||||||
 | 
					                return Seal(key.verifying, key.sign(message))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    companion object {
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * Create a new instance with a specific data sealed by one or more
 | 
				
			||||||
 | 
					         * keys. At least one key is required to disallow providing not-signed
 | 
				
			||||||
 | 
					         * instances, e.g. [SignedBox] is guaranteed to be properly sealed when
 | 
				
			||||||
 | 
					         * successfully instantiated.
 | 
				
			||||||
 | 
					         *
 | 
				
			||||||
 | 
					         * @param data a message to sign
 | 
				
			||||||
 | 
					         * @param keys a list of keys to sign with, should be at least one key.
 | 
				
			||||||
 | 
					         * @throws IllegalArgumentException if keys are not specified.
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        operator fun invoke(data: UByteArray, vararg keys: Key.Signing): SignedBox =
 | 
				
			||||||
 | 
					            SignedBox(data, keys.map { Seal.create(it, data) }, false)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										7
									
								
								src/commonMain/kotlin/net/sergeych/crypto/contrail.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/commonMain/kotlin/net/sergeych/crypto/contrail.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					package net.sergeych.crypto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import net.sergeych.bintools.CRC
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun isValidContrail(data: UByteArray): Boolean = CRC.crc8(data.copyOfRange(1, data.size)) == data[0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun createContrail(data: UByteArray): UByteArray = ubyteArrayOf(CRC.crc8(data)) + data
 | 
				
			||||||
							
								
								
									
										75
									
								
								src/commonMain/kotlin/net/sergeych/crypto/keys.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/commonMain/kotlin/net/sergeych/crypto/keys.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,75 @@
 | 
				
			|||||||
 | 
					package net.sergeych.crypto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.ionspin.kotlin.crypto.signature.InvalidSignatureException
 | 
				
			||||||
 | 
					import com.ionspin.kotlin.crypto.signature.Signature
 | 
				
			||||||
 | 
					import kotlinx.serialization.SerialName
 | 
				
			||||||
 | 
					import kotlinx.serialization.Serializable
 | 
				
			||||||
 | 
					import net.sergeych.crypto.Key.Signing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Keys in general: public, secret and later symmetric too.
 | 
				
			||||||
 | 
					 * Keys could be compared to each other for equality and used
 | 
				
			||||||
 | 
					 * as a Map keys (not sure about js).
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Use [Signing.pair] to create new keys.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					sealed class Key {
 | 
				
			||||||
 | 
					    abstract val packed: UByteArray
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun equals(other: Any?): Boolean {
 | 
				
			||||||
 | 
					        return other is Key && other.packed contentEquals packed
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun hashCode(): Int {
 | 
				
			||||||
 | 
					        return packed.contentHashCode()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun toString(): String = packed.encodeToBase64Url()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Public key to verify signatures only
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    @Serializable
 | 
				
			||||||
 | 
					    @SerialName("pvk")
 | 
				
			||||||
 | 
					    class Verifying(override val packed: UByteArray) : Key() {
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * Verify the signature and return true if it is correct.
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        fun verify(signature: UByteArray, message: UByteArray): Boolean = try {
 | 
				
			||||||
 | 
					            Signature.verifyDetached(signature, message, packed)
 | 
				
			||||||
 | 
					            true
 | 
				
			||||||
 | 
					        } catch (_: InvalidSignatureException) {
 | 
				
			||||||
 | 
					            false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        override fun toString(): String = "Pub:${super.toString()}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Secret key to sign only
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    @Serializable
 | 
				
			||||||
 | 
					    @SerialName("ssk")
 | 
				
			||||||
 | 
					    class Signing(override val packed: UByteArray) : Key() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val verifying: Verifying by lazy {
 | 
				
			||||||
 | 
					            Verifying(Signature.ed25519SkToPk(packed))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fun sign(message: UByteArray): UByteArray = Signature.detached(message, packed)
 | 
				
			||||||
 | 
					        override fun toString(): String = "Sct:${super.toString()}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        companion object {
 | 
				
			||||||
 | 
					            data class Pair(val signing: Signing, val verifying: Verifying)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            fun pair(): Pair {
 | 
				
			||||||
 | 
					                val p = Signature.keypair()
 | 
				
			||||||
 | 
					                return Pair(Signing(p.secretKey), Verifying(p.publicKey))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class IllegalSignatureException: RuntimeException("signed data is tampered or signature is corrupted")
 | 
				
			||||||
							
								
								
									
										85
									
								
								src/commonMain/kotlin/net/sergeych/crypto/tools.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/commonMain/kotlin/net/sergeych/crypto/tools.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,85 @@
 | 
				
			|||||||
 | 
					@file:Suppress("unused")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package net.sergeych.crypto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.ionspin.kotlin.crypto.secretbox.SecretBox
 | 
				
			||||||
 | 
					import com.ionspin.kotlin.crypto.secretbox.crypto_secretbox_NONCEBYTES
 | 
				
			||||||
 | 
					import com.ionspin.kotlin.crypto.util.LibsodiumRandom
 | 
				
			||||||
 | 
					import kotlinx.serialization.Serializable
 | 
				
			||||||
 | 
					import net.sergeych.bintools.toDataSource
 | 
				
			||||||
 | 
					import net.sergeych.bipack.BipackDecoder
 | 
				
			||||||
 | 
					import net.sergeych.bipack.BipackEncoder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DecryptionFailedException : RuntimeException("can't encrypt: wrong key or tampered message")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					data class WithNonce(
 | 
				
			||||||
 | 
					    val cipherData: UByteArray,
 | 
				
			||||||
 | 
					    val nonce: UByteArray,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					data class WithFill(
 | 
				
			||||||
 | 
					    val data: UByteArray,
 | 
				
			||||||
 | 
					    val safetyFill: UByteArray? = null
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    constructor(data: UByteArray, fillSize: Int) : this(data, randomBytes(fillSize))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun randomBytes(n: Int): UByteArray = if (n > 0) LibsodiumRandom.buf(n) else ubyteArrayOf()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun randomBytes(n: UInt): UByteArray = if (n > 0u) LibsodiumRandom.buf(n.toInt()) else ubyteArrayOf()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Uniform random in `0 ..< max` range
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					fun randomUInt(max: UInt) = LibsodiumRandom.uniform(max)
 | 
				
			||||||
 | 
					fun randomUInt(max: Int) = LibsodiumRandom.uniform(max.toUInt())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun <T: Comparable<T>>T.limit(range: ClosedRange<T>) = when {
 | 
				
			||||||
 | 
					    this < range.start -> range.start
 | 
				
			||||||
 | 
					    this > range.endInclusive -> range.endInclusive
 | 
				
			||||||
 | 
					    else -> this
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun <T: Comparable<T>>T.limitMax(max: T) = if( this < max ) this else max
 | 
				
			||||||
 | 
					fun <T: Comparable<T>>T.limitMin(min: T) = if( this > min ) this else min
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun randomNonce(): UByteArray = randomBytes(crypto_secretbox_NONCEBYTES)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Secret-key encrypt with authentication.
 | 
				
			||||||
 | 
					 * Generates random nonce and add some random fill to protect
 | 
				
			||||||
 | 
					 * against some analysis attacks. Nonce is included in the result. To be
 | 
				
			||||||
 | 
					 * used with [decrypt].
 | 
				
			||||||
 | 
					 * @param secretKey a _secret_ key, see [SecretBox.keygen()] or like.
 | 
				
			||||||
 | 
					 * @param plain data to encrypt
 | 
				
			||||||
 | 
					 * @param fillSize number of random fill data to add. Use random value or default.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					fun encrypt(
 | 
				
			||||||
 | 
					    secretKey: UByteArray,
 | 
				
			||||||
 | 
					    plain: UByteArray,
 | 
				
			||||||
 | 
					    fillSize: Int = randomUInt((plain.size * 3 / 10).limitMin(3)).toInt()
 | 
				
			||||||
 | 
					): UByteArray {
 | 
				
			||||||
 | 
					    val filled = BipackEncoder.encode(WithFill(plain, fillSize))
 | 
				
			||||||
 | 
					    val nonce = randomNonce()
 | 
				
			||||||
 | 
					    val encrypted = SecretBox.easy(filled.toUByteArray(), nonce, secretKey)
 | 
				
			||||||
 | 
					    return BipackEncoder.encode(WithNonce(encrypted, nonce)).toUByteArray()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Decrypt a secret-key-based message, normally encrypted with [encrypt].
 | 
				
			||||||
 | 
					 * @throws DecryptionFailedException if the key is wrong or a message is tampered with (MAC
 | 
				
			||||||
 | 
					 *          check failed).
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					fun decrypt(secretKey: UByteArray, cipher: UByteArray): UByteArray {
 | 
				
			||||||
 | 
					    val wn: WithNonce = BipackDecoder.decode(cipher.toDataSource())
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        return BipackDecoder.decode<WithFill>(
 | 
				
			||||||
 | 
					            SecretBox.openEasy(wn.cipherData, wn.nonce, secretKey).toDataSource()
 | 
				
			||||||
 | 
					        ).data
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    catch(_: com.ionspin.kotlin.crypto.secretbox.SecretBoxCorruptedOrTamperedDataExceptionOrInvalidKey) {
 | 
				
			||||||
 | 
					        throw DecryptionFailedException()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										8
									
								
								src/commonMain/kotlin/net/sergeych/crypto/utools.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/commonMain/kotlin/net/sergeych/crypto/utools.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					package net.sergeych.crypto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import net.sergeych.bintools.toDump
 | 
				
			||||||
 | 
					import net.sergeych.mp_tools.encodeToBase64Url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun UByteArray.toDump(wide: Boolean = false) = toByteArray().toDump(wide)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun UByteArray.encodeToBase64Url(): String = toByteArray().encodeToBase64Url()
 | 
				
			||||||
@ -1,47 +0,0 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import io.ktor.utils.io.*
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
object AsyncVarint {
 | 
					 | 
				
			||||||
    suspend fun encodeUnsigned(value: ULong, output: ByteWriteChannel) {
 | 
					 | 
				
			||||||
        var rest = value
 | 
					 | 
				
			||||||
        do {
 | 
					 | 
				
			||||||
            val x = (rest and 127u).toInt()
 | 
					 | 
				
			||||||
            rest = rest shr 7
 | 
					 | 
				
			||||||
            if (rest > 0u)
 | 
					 | 
				
			||||||
                output.writeByte(x or 0x80)
 | 
					 | 
				
			||||||
            else
 | 
					 | 
				
			||||||
                output.writeByte(x)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        } while (rest > 0u)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    suspend fun decodeUnsigned(source: ByteReadChannel): ULong {
 | 
					 | 
				
			||||||
        var result: ULong = 0u
 | 
					 | 
				
			||||||
        var count = 0
 | 
					 | 
				
			||||||
        while (true) {
 | 
					 | 
				
			||||||
            val x = source.readUByte().toInt()
 | 
					 | 
				
			||||||
            result = result or ((x and 0x7F).toULong() shl count)
 | 
					 | 
				
			||||||
            if ((x and 0x80) == 0)
 | 
					 | 
				
			||||||
                break
 | 
					 | 
				
			||||||
            count += 7
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return result
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
suspend fun ByteReadChannel.readUByte(): UByte = this.readByte().toUByte()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
suspend fun ByteWriteChannel.writeByte(byte: Int) {
 | 
					 | 
				
			||||||
    this.writeByte(byte.toByte())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,52 +0,0 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import kotlinx.coroutines.sync.Mutex
 | 
					 | 
				
			||||||
import kotlinx.coroutines.sync.withLock
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Multiplatform atomically mutable value to be used in [kotlinx.coroutines],
 | 
					 | 
				
			||||||
 * with suspending mutating operations, see [mutate].
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * Actual value can be either changed in a block of [mutate] when
 | 
					 | 
				
			||||||
 * new value _depends on the current value_ or with [reset].
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * [value] getter is suspended because it waits until the mutation finishes
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
open class AtomicAsyncValue<T>(initialValue: T) {
 | 
					 | 
				
			||||||
    private var actualValue = initialValue
 | 
					 | 
				
			||||||
    private val access = Mutex()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Change the value: get the current and set to the returned, all in the
 | 
					 | 
				
			||||||
     * atomic suspend operation. All other mutating requests including assigning to [value]
 | 
					 | 
				
			||||||
     * will be blocked and queued.
 | 
					 | 
				
			||||||
     * @return result of the mutation. Note that immediate call to property [value]
 | 
					 | 
				
			||||||
     *      could already return modified bu some other thread value!
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    suspend fun mutate(mutator: suspend (T) -> T): T = access.withLock {
 | 
					 | 
				
			||||||
        actualValue = mutator(actualValue)
 | 
					 | 
				
			||||||
        actualValue
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Atomic get or set the value. Atomic get means if there is a [mutate] in progress
 | 
					 | 
				
			||||||
     * it will wait until the mutation finishes and then return the correct result.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    suspend fun value() = access.withLock { actualValue }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Set the new value without checking it. Shortcut to
 | 
					 | 
				
			||||||
     * ```mutate { value = newValue }```
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    suspend fun reset(value: T) = mutate { value }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,13 +1,3 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec
 | 
					package net.sergeych.kiloparsec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import kotlinx.serialization.KSerializer
 | 
					import kotlinx.serialization.KSerializer
 | 
				
			||||||
@ -15,8 +5,6 @@ import kotlinx.serialization.Serializable
 | 
				
			|||||||
import net.sergeych.bintools.toDataSource
 | 
					import net.sergeych.bintools.toDataSource
 | 
				
			||||||
import net.sergeych.bipack.BipackDecoder
 | 
					import net.sergeych.bipack.BipackDecoder
 | 
				
			||||||
import net.sergeych.bipack.BipackEncoder
 | 
					import net.sergeych.bipack.BipackEncoder
 | 
				
			||||||
import net.sergeych.kiloparsec.Command.Call
 | 
					 | 
				
			||||||
import net.sergeych.kiloparsec.Command.Companion.unpackCall
 | 
					 | 
				
			||||||
import net.sergeych.utools.unpack
 | 
					import net.sergeych.utools.unpack
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@ -24,20 +12,8 @@ import net.sergeych.utools.unpack
 | 
				
			|||||||
 * in node-2-node protocols and client API, and most importantly in calling smart contract
 | 
					 * in node-2-node protocols and client API, and most importantly in calling smart contract
 | 
				
			||||||
 * methods. This is essentially a Kotlin binding to typesafe serialize command calls and
 | 
					 * methods. This is essentially a Kotlin binding to typesafe serialize command calls and
 | 
				
			||||||
 * deserialize results.
 | 
					 * deserialize results.
 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To create command instances, it is recommended to use [command] that returns [CommandDelegate].
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * Use [packCall] to serialize the command call with some arguments.
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * Note that `Command` instances themselves are not serialized, instead, the call is serialized,
 | 
					 | 
				
			||||||
 * in the form of [Call], containing name and properly serialized arguments.
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * [unpackCall] deserialized result of the [packCall] so the proper handler for the command could
 | 
					 | 
				
			||||||
 * be used. Then the result of the execution could be packed with [exec] and then unpacked with
 | 
					 | 
				
			||||||
 * [unpackResult].
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
open class Command<A, R>(
 | 
					class Command<A, R>(
 | 
				
			||||||
    val name: String,
 | 
					    val name: String,
 | 
				
			||||||
    val argsSerializer: KSerializer<A>,
 | 
					    val argsSerializer: KSerializer<A>,
 | 
				
			||||||
    val resultSerializer: KSerializer<R>
 | 
					    val resultSerializer: KSerializer<R>
 | 
				
			||||||
@ -45,41 +21,20 @@ open class Command<A, R>(
 | 
				
			|||||||
    @Serializable
 | 
					    @Serializable
 | 
				
			||||||
    data class Call(val name: String,val serializedArgs: UByteArray)
 | 
					    data class Call(val name: String,val serializedArgs: UByteArray)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    fun packCall(args: A): UByteArray = BipackEncoder.encode(
 | 
				
			||||||
     * Pack command invocation with specified arguments.
 | 
					        Call(name, BipackEncoder.encode(argsSerializer, args).toUByteArray())
 | 
				
			||||||
     */
 | 
					    ).toUByteArray()
 | 
				
			||||||
    fun packCall(args: A): UByteArray = BipackEncoder.encode(createCall(args)).toUByteArray()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Create [Call] instance for specified args vy serializing it properly
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    fun createCall(args: A): Call = Call(name, BipackEncoder.encode(argsSerializer, args).toUByteArray())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Unpack result, obtained by [exec].
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    fun unpackResult(packedResult: UByteArray): R =
 | 
					    fun unpackResult(packedResult: UByteArray): R =
 | 
				
			||||||
        unpack(resultSerializer, packedResult)
 | 
					        unpack(resultSerializer, packedResult)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Execute a command unpacking args.
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param packedArgs arguments, as provided by [packCall] in the [Call] instance
 | 
					 | 
				
			||||||
     * @param handler actual code to execute the command
 | 
					 | 
				
			||||||
     * @return properly serialized result to be unpacked with [unpackResult].
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    suspend fun exec(packedArgs: UByteArray, handler: suspend (A) -> R): UByteArray =
 | 
					    suspend fun exec(packedArgs: UByteArray, handler: suspend (A) -> R): UByteArray =
 | 
				
			||||||
        BipackEncoder.encode(
 | 
					        BipackEncoder.encode(
 | 
				
			||||||
            resultSerializer,
 | 
					            resultSerializer,
 | 
				
			||||||
            handler(
 | 
					            handler(BipackDecoder.decode(packedArgs.toDataSource(), argsSerializer))
 | 
				
			||||||
                BipackDecoder.decode(packedArgs.toDataSource(), argsSerializer))
 | 
					 | 
				
			||||||
        ).toUByteArray()
 | 
					        ).toUByteArray()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    companion object {
 | 
					    companion object {
 | 
				
			||||||
        /**
 | 
					 | 
				
			||||||
         * Unpack command invocation instance from [packCall]. Use [exec] to deserialize arguments and
 | 
					 | 
				
			||||||
         * perform command.
 | 
					 | 
				
			||||||
         */
 | 
					 | 
				
			||||||
        fun unpackCall(packedCall: UByteArray): Call = BipackDecoder.decode(packedCall.toDataSource())
 | 
					        fun unpackCall(packedCall: UByteArray): Call = BipackDecoder.decode(packedCall.toDataSource())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -1,13 +1,3 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec
 | 
					package net.sergeych.kiloparsec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import kotlinx.serialization.KSerializer
 | 
					import kotlinx.serialization.KSerializer
 | 
				
			||||||
@ -17,9 +7,6 @@ import kotlin.reflect.KProperty
 | 
				
			|||||||
/**
 | 
					/**
 | 
				
			||||||
 * delegate returning function that creates a [Command] in the current context which by default has the name of
 | 
					 * delegate returning function that creates a [Command] in the current context which by default has the name of
 | 
				
			||||||
 * the property.
 | 
					 * the property.
 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * The Default name is a property name except the "cmd" prefix if present, which will be
 | 
					 | 
				
			||||||
 * removed automatically.
 | 
					 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
inline fun <reified A, reified R> command(overrideName: String? = null): CommandDelegate<A, R> {
 | 
					inline fun <reified A, reified R> command(overrideName: String? = null): CommandDelegate<A, R> {
 | 
				
			||||||
    return CommandDelegate(
 | 
					    return CommandDelegate(
 | 
				
			||||||
@ -29,33 +16,19 @@ inline fun <reified A, reified R> command(overrideName: String? = null): Command
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Declare a Push: Unit-returning command usually used with [RemoteInterface.push]
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
inline fun <reified A> push(overrideName: String? = null): CommandDelegate<A, Unit>  = command(overrideName)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Delegate to create [Command] via property
 | 
					 * Delegate to create [Command] via property
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class CommandDelegate<A, R>(
 | 
					class CommandDelegate<A, R>(
 | 
				
			||||||
    private val argsSerializer: KSerializer<A>,
 | 
					    private val argsSerializer: KSerializer<A>,
 | 
				
			||||||
    private val resultSerializer: KSerializer<R>,
 | 
					    private val resultSerializer: KSerializer<R>,
 | 
				
			||||||
    private val overrideName: String? = null,
 | 
					    private val overrideName: String? = null
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
    private var name: String = ""
 | 
					 | 
				
			||||||
    operator fun getValue(nothing: Nothing?, property: KProperty<*>): Command<A, R> {
 | 
					    operator fun getValue(nothing: Nothing?, property: KProperty<*>): Command<A, R> {
 | 
				
			||||||
        if (name.isEmpty()) {
 | 
					 | 
				
			||||||
            name = overrideName ?: removeCmd(property.name)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return Command(
 | 
					        return Command(
 | 
				
			||||||
            name,
 | 
					            overrideName ?: property.name,
 | 
				
			||||||
            argsSerializer,
 | 
					            argsSerializer,
 | 
				
			||||||
            resultSerializer
 | 
					            resultSerializer
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
private fun removeCmd(name: String) =
 | 
					 | 
				
			||||||
    if (name.startsWith("cmd"))
 | 
					 | 
				
			||||||
        name.substring(3)
 | 
					 | 
				
			||||||
    else name
 | 
					 | 
				
			||||||
@ -1,13 +1,3 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec
 | 
					package net.sergeych.kiloparsec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 | 
				
			|||||||
@ -1,20 +0,0 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * External errors wich needs to be encoded with a specific code
 | 
					 | 
				
			||||||
 * should implement this interface so it will be serialized properly.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
interface ExceptionWithCode {
 | 
					 | 
				
			||||||
    val code: String
 | 
					 | 
				
			||||||
    val message: String?
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,26 +1,11 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec
 | 
					package net.sergeych.kiloparsec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import kotlinx.coroutines.CancellationException
 | 
					import kotlinx.coroutines.CancellationException
 | 
				
			||||||
import kotlinx.coroutines.CompletableDeferred
 | 
					import kotlinx.coroutines.CompletableDeferred
 | 
				
			||||||
import kotlinx.coroutines.delay
 | 
					 | 
				
			||||||
import kotlinx.coroutines.flow.MutableStateFlow
 | 
					import kotlinx.coroutines.flow.MutableStateFlow
 | 
				
			||||||
import kotlinx.coroutines.flow.asStateFlow
 | 
					import kotlinx.coroutines.flow.asStateFlow
 | 
				
			||||||
import kotlinx.coroutines.isActive
 | 
					import kotlinx.coroutines.isActive
 | 
				
			||||||
import kotlinx.coroutines.sync.Mutex
 | 
					import net.sergeych.crypto.Key
 | 
				
			||||||
import kotlinx.coroutines.sync.withLock
 | 
					 | 
				
			||||||
import net.sergeych.crypto2.SigningKey
 | 
					 | 
				
			||||||
import net.sergeych.crypto2.VerifyingKey
 | 
					 | 
				
			||||||
import net.sergeych.crypto2.VerifyingPublicKey
 | 
					 | 
				
			||||||
import net.sergeych.mp_logger.LogTag
 | 
					import net.sergeych.mp_logger.LogTag
 | 
				
			||||||
import net.sergeych.mp_logger.Loggable
 | 
					import net.sergeych.mp_logger.Loggable
 | 
				
			||||||
import net.sergeych.mp_logger.debug
 | 
					import net.sergeych.mp_logger.debug
 | 
				
			||||||
@ -28,40 +13,18 @@ import net.sergeych.mp_logger.exception
 | 
				
			|||||||
import net.sergeych.mp_tools.globalLaunch
 | 
					import net.sergeych.mp_tools.globalLaunch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * The auto-connecting client that reconnects to the kiloparsec server,
 | 
					 * The auto-connecting client that reconnects to the kiloparsec server
 | 
				
			||||||
 * [KiloServer],
 | 
					 | 
				
			||||||
 * and maintain connection state flow. Client factory launches a disconnected
 | 
					 * and maintain connection state flow. Client factory launches a disconnected
 | 
				
			||||||
 * set of coroutines to support automatic reconnection, so you _must_ [close]
 | 
					 * set of coroutines to support automatic reconnection, so you _must_ [close]
 | 
				
			||||||
 * it manually when it is unnecessary, otherwise it will continue to reconnect.
 | 
					 * it manually when it is not needed, otherwise it will continue to reconnect.
 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * ## Usage
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * Suppose we have TCP/IP server as in the [KiloServer] usage sample. Then we can connect
 | 
					 | 
				
			||||||
 * to it providing TCP/IP connector like:
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * ```kotlin
 | 
					 | 
				
			||||||
 * val client = KiloClient<Unit>() {
 | 
					 | 
				
			||||||
 *     connect { connectTcpDevice("localhost:$port") }
 | 
					 | 
				
			||||||
 * }
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * // now we can invoke remote commands:
 | 
					 | 
				
			||||||
 * assertEquals("unknown", client.call(cmdLoad))
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * client.call(cmdSave, "foobar")
 | 
					 | 
				
			||||||
 * assertEquals("foobar", client.call(cmdLoad))
 | 
					 | 
				
			||||||
 * ```
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * ## See also
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * [KiloServer]
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class KiloClient<S>(
 | 
					class KiloClient<S>(
 | 
				
			||||||
    val localInterface: KiloInterface<S>,
 | 
					    localInterface: KiloInterface<S>,
 | 
				
			||||||
    secretKey: SigningKey? = null,
 | 
					    secretKey: Key.Signing? = null,
 | 
				
			||||||
    connectionDataFactory: ConnectionDataFactory<S>,
 | 
					    connectionDataFactory: ConnectionDataFactory<S>,
 | 
				
			||||||
) : RemoteInterface,
 | 
					) : RemoteInterface,
 | 
				
			||||||
    Loggable by LogTag("CLIF") {
 | 
					    Loggable by LogTag("CLIF") {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val _state = MutableStateFlow(false)
 | 
					    val _state = MutableStateFlow(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@ -69,15 +32,7 @@ class KiloClient<S>(
 | 
				
			|||||||
     * to authenticate a client on connection restore, for example.
 | 
					     * to authenticate a client on connection restore, for example.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    @Suppress("unused")
 | 
					    @Suppress("unused")
 | 
				
			||||||
    val connectedStateFlow = _state.asStateFlow()
 | 
					    val state = _state.asStateFlow()
 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * The verifying, or public, key identifying client sessions. It could be used to
 | 
					 | 
				
			||||||
     * restore environment on reconnection. This is what remote side, e.g. server, sees as
 | 
					 | 
				
			||||||
     * [KiloScope.remoteIdentity].
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    @Suppress("unused")
 | 
					 | 
				
			||||||
    val localIdentity: VerifyingKey? = secretKey?.verifyingKey
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private var deferredClient = CompletableDeferred<KiloClientConnection<S>>()
 | 
					    private var deferredClient = CompletableDeferred<KiloClientConnection<S>>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -89,63 +44,30 @@ class KiloClient<S>(
 | 
				
			|||||||
                    debug { "getting connection" }
 | 
					                    debug { "getting connection" }
 | 
				
			||||||
                    val kc = connectionDataFactory()
 | 
					                    val kc = connectionDataFactory()
 | 
				
			||||||
                    debug { "get device and session" }
 | 
					                    debug { "get device and session" }
 | 
				
			||||||
                    val client = KiloClientConnection(localInterface, kc, secretKey)
 | 
					                    val client = KiloClientConnection(localInterface, kc,secretKey)
 | 
				
			||||||
                    deferredClient.complete(client)
 | 
					                    deferredClient.complete(client)
 | 
				
			||||||
                    debug { "starting client run"}
 | 
					                    client.run {
 | 
				
			||||||
                    val r = runCatching {  client.run {
 | 
					                        _state.value = false
 | 
				
			||||||
                        _state.value = it
 | 
					                    }
 | 
				
			||||||
                    } }
 | 
					 | 
				
			||||||
                    debug { "----------- client run finished: $r" }
 | 
					 | 
				
			||||||
                    resetDeferredClient()
 | 
					 | 
				
			||||||
                    debug { "client run finished" }
 | 
					                    debug { "client run finished" }
 | 
				
			||||||
                } catch (_: RemoteInterface.ClosedException) {
 | 
					                } catch (_: RemoteInterface.ClosedException) {
 | 
				
			||||||
                    debug { "remote closed" }
 | 
					                    debug { "remote closed" }
 | 
				
			||||||
                    delay(1000)
 | 
					 | 
				
			||||||
                } catch (_: CancellationException) {
 | 
					                } catch (_: CancellationException) {
 | 
				
			||||||
                    debug { "cancelled" }
 | 
					                    debug { "cancelled" }
 | 
				
			||||||
                    break
 | 
					 | 
				
			||||||
                } catch (t: Throwable) {
 | 
					                } catch (t: Throwable) {
 | 
				
			||||||
                    exception { "unexpected exception" to t }
 | 
					                    exception { "unexpected exception" to t }
 | 
				
			||||||
                    delay(1000)
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                _state.value = false
 | 
					                _state.value = false
 | 
				
			||||||
                resetDeferredClient()
 | 
					                if (deferredClient.isActive)
 | 
				
			||||||
                // reconnection timeout
 | 
					                    deferredClient = CompletableDeferred()
 | 
				
			||||||
                delay(700)
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fun close() {
 | 
					    fun close() {
 | 
				
			||||||
        job.cancel()
 | 
					        job.cancel()
 | 
				
			||||||
        debug { "client is closed" }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private val defMutex = Mutex()
 | 
					    override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R = deferredClient.await().call(cmd, args)
 | 
				
			||||||
    private suspend fun resetDeferredClient() {
 | 
					 | 
				
			||||||
        defMutex.withLock {
 | 
					 | 
				
			||||||
            if (!deferredClient.isActive) {
 | 
					 | 
				
			||||||
                deferredClient = CompletableDeferred()
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R =
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
            deferredClient.await().call(cmd, args)
 | 
					 | 
				
			||||||
        } catch (t: RemoteInterface.ClosedException) {
 | 
					 | 
				
			||||||
            resetDeferredClient()
 | 
					 | 
				
			||||||
            throw t
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    override suspend fun <A> push(cmd: Command<A, Unit>, args: A) {
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
            deferredClient.await().push(cmd, args)
 | 
					 | 
				
			||||||
        } catch (t: RemoteInterface.ClosedException) {
 | 
					 | 
				
			||||||
            resetDeferredClient()
 | 
					 | 
				
			||||||
            throw t
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Current session token. This is a per-connection unique random value same on the client and server part so
 | 
					     * Current session token. This is a per-connection unique random value same on the client and server part so
 | 
				
			||||||
@ -155,14 +77,14 @@ class KiloClient<S>(
 | 
				
			|||||||
    suspend fun token() = deferredClient.await().token()
 | 
					    suspend fun token() = deferredClient.await().token()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Remote party shared key ([VerifyingPublicKey]), could be used ti ensure server is what we expected and
 | 
					     * Remote party shared key ([Key.Verifying]]), could be used ti ensure server is what we expected and
 | 
				
			||||||
     * there is no active MITM attack.
 | 
					     * there is no active MITM attack.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * Non-null value means the key was successfully authenticated, null means remote party did not provide
 | 
					     * Non-null value means the key was successfully authenticated, null means remote party did not provide
 | 
				
			||||||
     * a key. Connection is established either with a properly authenticated key or no key at all.
 | 
					     * a key. Connection is established either with a properly authenticated key or no key at all.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    @Suppress("unused")
 | 
					    @Suppress("unused")
 | 
				
			||||||
    suspend fun remoteIdentity(): VerifyingPublicKey? = deferredClient.await().remoteId()
 | 
					    suspend fun remoteId() = deferredClient.await().remoteId()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    companion object {
 | 
					    companion object {
 | 
				
			||||||
        class Builder<S>() {
 | 
					        class Builder<S>() {
 | 
				
			||||||
@ -174,42 +96,16 @@ class KiloClient<S>(
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
            private var connectionBuilder: (suspend () -> Transport.Device)? = null
 | 
					            private var connectionBuilder: (suspend () -> Transport.Device)? = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var secretIdKey: SigningKey? = null
 | 
					            var secretIdKey: Key.Signing? = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            /**
 | 
					            /**
 | 
				
			||||||
             * Build local command implementations, those callable from the server, exception
 | 
					             * Build local command implementations (remotely callable ones), exception
 | 
				
			||||||
             * class handlers, and anything else [KiloInterface] allows. Usage sample:
 | 
					             * class handlers, etc.
 | 
				
			||||||
             * 
 | 
					 | 
				
			||||||
             * ```kotlin
 | 
					 | 
				
			||||||
             * val client = KiloClient {
 | 
					 | 
				
			||||||
             *     connect { connectTcpDevice("localhost:$port") }
 | 
					 | 
				
			||||||
             *     local {
 | 
					 | 
				
			||||||
             *         on(cmdPing) {
 | 
					 | 
				
			||||||
             *             "pong! $it"
 | 
					 | 
				
			||||||
             *         }
 | 
					 | 
				
			||||||
             *     }
 | 
					 | 
				
			||||||
             * }
 | 
					 | 
				
			||||||
             * ```
 | 
					 | 
				
			||||||
             */
 | 
					             */
 | 
				
			||||||
            fun local(f: KiloInterface<S>.() -> Unit) {
 | 
					            fun local(f: KiloInterface<S>.() -> Unit) {
 | 
				
			||||||
                interfaceBuilder = f
 | 
					                interfaceBuilder = f
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            /**
 | 
					 | 
				
			||||||
             * Import exception (error) registrations from a shared LocalInterface (usually, server side one) to
 | 
					 | 
				
			||||||
             * DRY error registration on both server and client sides.
 | 
					 | 
				
			||||||
             * Normally you need to:
 | 
					 | 
				
			||||||
             *
 | 
					 | 
				
			||||||
             * - Register all your errors in server's LocalInterface
 | 
					 | 
				
			||||||
             *
 | 
					 | 
				
			||||||
             * - In the client builder call [addErrors(serverLocalInterface]
 | 
					 | 
				
			||||||
             *
 | 
					 | 
				
			||||||
             * See also [LocalInterface.registerError] for exception registration
 | 
					 | 
				
			||||||
             */
 | 
					 | 
				
			||||||
            fun addErrors(from: LocalInterface<*>) {
 | 
					 | 
				
			||||||
                errorProviders += from
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            /**
 | 
					            /**
 | 
				
			||||||
             * Create a new session object, otherwise Unit session will be used
 | 
					             * Create a new session object, otherwise Unit session will be used
 | 
				
			||||||
             */
 | 
					             */
 | 
				
			||||||
@ -221,15 +117,12 @@ class KiloClient<S>(
 | 
				
			|||||||
                connectionBuilder = f
 | 
					                connectionBuilder = f
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            val errorProviders = mutableListOf<LocalInterface<*>>()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            internal fun build(): KiloClient<S> {
 | 
					            internal fun build(): KiloClient<S> {
 | 
				
			||||||
                val i = KiloInterface<S>()
 | 
					                val i = KiloInterface<S>()
 | 
				
			||||||
                for (ep in errorProviders) i.addErrorProvider(ep)
 | 
					 | 
				
			||||||
                interfaceBuilder?.let { i.it() }
 | 
					                interfaceBuilder?.let { i.it() }
 | 
				
			||||||
                val connector = connectionBuilder ?: throw IllegalArgumentException("connect handler was not set")
 | 
					                val connector = connectionBuilder ?: throw IllegalArgumentException("connect handler was not set")
 | 
				
			||||||
                return KiloClient(i, secretIdKey) {
 | 
					                return KiloClient(i,secretIdKey) {
 | 
				
			||||||
                    KiloConnectionData(connector(), sessionBuilder())
 | 
					                    KiloConnectionData(connector(),sessionBuilder())
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,19 +1,8 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec
 | 
					package net.sergeych.kiloparsec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.ionspin.kotlin.crypto.keyexchange.KeyExchange
 | 
				
			||||||
import kotlinx.coroutines.*
 | 
					import kotlinx.coroutines.*
 | 
				
			||||||
import net.sergeych.crypto2.SafeKeyExchange
 | 
					import net.sergeych.crypto.Key
 | 
				
			||||||
import net.sergeych.crypto2.SigningKey
 | 
					 | 
				
			||||||
import net.sergeych.crypto2.VerifyingPublicKey
 | 
					 | 
				
			||||||
import net.sergeych.mp_logger.LogTag
 | 
					import net.sergeych.mp_logger.LogTag
 | 
				
			||||||
import net.sergeych.mp_logger.Loggable
 | 
					import net.sergeych.mp_logger.Loggable
 | 
				
			||||||
import net.sergeych.mp_logger.debug
 | 
					import net.sergeych.mp_logger.debug
 | 
				
			||||||
@ -23,20 +12,20 @@ import net.sergeych.utools.pack
 | 
				
			|||||||
private var clientIds = 0
 | 
					private var clientIds = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class KiloClientConnection<S>(
 | 
					class KiloClientConnection<S>(
 | 
				
			||||||
    private val clientInterface: KiloInterface<S>,
 | 
					    private val clientInterface: LocalInterface<KiloScope<S>>,
 | 
				
			||||||
    private val device: Transport.Device,
 | 
					    private val device: Transport.Device,
 | 
				
			||||||
    private val session: S,
 | 
					    private val session: S,
 | 
				
			||||||
    private val secretIdKey: SigningKey? = null,
 | 
					    private val secretIdKey: Key.Signing? = null,
 | 
				
			||||||
) : RemoteInterface, Loggable by LogTag("KPC:${++clientIds}") {
 | 
					) : RemoteInterface, Loggable by LogTag("KPC:${++clientIds}") {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(localInterface: KiloInterface<S>, connection: KiloConnectionData<S>, secretIdKey: SigningKey? = null)
 | 
					    constructor(localInterface: KiloInterface<S>, connection: KiloConnectionData<S>, secretIdKey: Key.Signing? = null)
 | 
				
			||||||
            : this(localInterface, connection.device, connection.session, secretIdKey)
 | 
					            : this(localInterface, connection.device, connection.session, secretIdKey)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private val kiloRemoteInterface = CompletableDeferred<KiloRemoteInterface<S>>()
 | 
					    private val kiloRemoteInterface = CompletableDeferred<KiloRemoteInterface<S>>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private val deferredParams = CompletableDeferred<KiloParams<S>>()
 | 
					    private val deferredParams = CompletableDeferred<KiloParams<S>>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    suspend fun remoteId(): VerifyingPublicKey? = deferredParams.await().remoteIdentity
 | 
					    suspend fun remoteId(): Key.Verifying? = deferredParams.await().remoteIdentity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Run the client, blocking until the device is closed, or some critical exception
 | 
					     * Run the client, blocking until the device is closed, or some critical exception
 | 
				
			||||||
@ -48,7 +37,8 @@ class KiloClientConnection<S>(
 | 
				
			|||||||
            var job: Job? = null
 | 
					            var job: Job? = null
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
                // in parallel: keys and connection
 | 
					                // in parallel: keys and connection
 | 
				
			||||||
                val deferredKeyPair = async { SafeKeyExchange() }
 | 
					                val deferredKeyPair = async { KeyExchange.keypair() }
 | 
				
			||||||
 | 
					                debug { "opening device" }
 | 
				
			||||||
                debug { "got a transport device $device" }
 | 
					                debug { "got a transport device $device" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -61,26 +51,27 @@ class KiloClientConnection<S>(
 | 
				
			|||||||
                debug { "transport started" }
 | 
					                debug { "transport started" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                val pair = deferredKeyPair.await()
 | 
					                val pair = deferredKeyPair.await()
 | 
				
			||||||
                debug { "keypair ready (1)" }
 | 
					                debug { "keypair ready" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                val serverHe = transport.call(L0Request, Handshake(1u, pair.publicKey))
 | 
					                val serverHe = transport.call(L0Request, Handshake(1u, pair.publicKey))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                debug { "got server HE (2)" }
 | 
					                val sk = KeyExchange.clientSessionKeys(pair.publicKey, pair.secretKey, serverHe.publicKey)
 | 
				
			||||||
                val sk = pair.clientSessionKey(serverHe.publicKey)
 | 
					 | 
				
			||||||
                var params = KiloParams(false, transport, sk, session, null, this@KiloClientConnection)
 | 
					                var params = KiloParams(false, transport, sk, session, null, this@KiloClientConnection)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // Check ID if any
 | 
					                // Check ID if any
 | 
				
			||||||
                serverHe.signature?.let { s ->
 | 
					                serverHe.serverSharedKey?.let { k ->
 | 
				
			||||||
                    if (!s.isValid(params.token))
 | 
					                    if (serverHe.signature == null)
 | 
				
			||||||
 | 
					                        throw RemoteInterface.SecurityException("missing signature")
 | 
				
			||||||
 | 
					                    if (!k.verify(serverHe.signature, params.token))
 | 
				
			||||||
                        throw RemoteInterface.SecurityException("wrong signature")
 | 
					                        throw RemoteInterface.SecurityException("wrong signature")
 | 
				
			||||||
                    params = params.copy(remoteIdentity = s.publicKey)
 | 
					                    params = params.copy(remoteIdentity = k)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                transport.call(
 | 
					                transport.call(
 | 
				
			||||||
                    L0ClientId, params.encrypt(
 | 
					                    L0ClientId, params.encrypt(
 | 
				
			||||||
                        pack(
 | 
					                        pack(
 | 
				
			||||||
                            ClientIdentity(
 | 
					                            ClientIdentity(
 | 
				
			||||||
                                secretIdKey?.verifyingKey,
 | 
					                                secretIdKey?.verifying,
 | 
				
			||||||
                                secretIdKey?.sign(params.token)
 | 
					                                secretIdKey?.sign(params.token)
 | 
				
			||||||
                            )
 | 
					                            )
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
@ -90,14 +81,13 @@ class KiloClientConnection<S>(
 | 
				
			|||||||
                kiloRemoteInterface.complete(
 | 
					                kiloRemoteInterface.complete(
 | 
				
			||||||
                    KiloRemoteInterface(deferredParams, clientInterface)
 | 
					                    KiloRemoteInterface(deferredParams, clientInterface)
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                clientInterface.onConnectHandlers.invokeAll(params.scope)
 | 
					 | 
				
			||||||
                onConnectedStateChanged?.invoke(true)
 | 
					                onConnectedStateChanged?.invoke(true)
 | 
				
			||||||
                job.join()
 | 
					                job.join()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            } catch (x: CancellationException) {
 | 
					            } catch (x: CancellationException) {
 | 
				
			||||||
                info { "client is cancelled" }
 | 
					                info { "client is cancelled" }
 | 
				
			||||||
            } catch (x: RemoteInterface.ClosedException) {
 | 
					            } catch (x: RemoteInterface.ClosedException) {
 | 
				
			||||||
                debug { "connection closed/refused by remote" }
 | 
					                info { "connection closed by remote" }
 | 
				
			||||||
            } finally {
 | 
					            } finally {
 | 
				
			||||||
                onConnectedStateChanged?.invoke(false)
 | 
					                onConnectedStateChanged?.invoke(false)
 | 
				
			||||||
                job?.cancel()
 | 
					                job?.cancel()
 | 
				
			||||||
@ -109,11 +99,4 @@ class KiloClientConnection<S>(
 | 
				
			|||||||
    suspend fun token() = deferredParams.await().token
 | 
					    suspend fun token() = deferredParams.await().token
 | 
				
			||||||
    override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R =
 | 
					    override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R =
 | 
				
			||||||
        kiloRemoteInterface.await().call(cmd, args)
 | 
					        kiloRemoteInterface.await().call(cmd, args)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
    override suspend fun <A> push(cmd: Command<A, Unit>, args: A) {
 | 
					 | 
				
			||||||
        kiloRemoteInterface.await().push(cmd, args)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
internal fun <S>Collection<KiloHandler<S>>.invokeAll(scope: KiloScope<S>) =
 | 
					 | 
				
			||||||
    forEach { runCatching { scope.it() } }
 | 
					 | 
				
			||||||
@ -1,44 +1,14 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec
 | 
					package net.sergeych.kiloparsec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
typealias KiloHandler<S> = KiloScope<S>.()->Unit
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * The local interface to provide functions, register errors for Kiloparsec users. Use it
 | 
					 * The local interface to provice functions, register errors for Kiloparsec users. Use it
 | 
				
			||||||
 * with [KiloClient], [KiloClientConnection], [KiloServerConnection], etc.
 | 
					 * with [KiloClient], [KiloClientConnection], [KiloServerConnection], etc.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * Base class implementation does the following:
 | 
					 * BAse implementation registers relevant exceptions.
 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * - It registers common exceptions from [RemoteInterface] and kotlin/java `IllegalArgumentException` and
 | 
					 | 
				
			||||||
 *      `IllegalStateException`
 | 
					 | 
				
			||||||
 * - It provides [onConnected] handler
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * See [KiloServer] for usage sample.
 | 
					 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
open class KiloInterface<S> : LocalInterface<KiloScope<S>>() {
 | 
					class KiloInterface<S> : LocalInterface<KiloScope<S>>() {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    internal val onConnectHandlers = mutableListOf<KiloHandler<S>>()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Registers handler [f] for [onConnected] event, to the head or the end of handler list.
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param addFirst if true, [f] will be added to the beginning of the list of handlers
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    fun onConnected(addFirst: Boolean = false, f: KiloScope<S>.()->Unit) {
 | 
					 | 
				
			||||||
        if( addFirst ) onConnectHandlers.add(0, f) else  onConnectHandlers += f
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    init {
 | 
					    init {
 | 
				
			||||||
        registerError { RemoteInterface.UnknownCommand(it) }
 | 
					        registerError { RemoteInterface.UnknownCommand() }
 | 
				
			||||||
        registerError { RemoteInterface.InternalError(it) }
 | 
					 | 
				
			||||||
        registerError { RemoteInterface.ClosedException(it) }
 | 
					        registerError { RemoteInterface.ClosedException(it) }
 | 
				
			||||||
        registerError { RemoteInterface.SecurityException(it) }
 | 
					        registerError { RemoteInterface.SecurityException(it) }
 | 
				
			||||||
        registerError { RemoteInterface.InvalidDataException(it) }
 | 
					        registerError { RemoteInterface.InvalidDataException(it) }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,21 +1,8 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec
 | 
					package net.sergeych.kiloparsec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import kotlinx.coroutines.CompletableDeferred
 | 
					import kotlinx.coroutines.CompletableDeferred
 | 
				
			||||||
import net.sergeych.tools.AtomicCounter
 | 
					 | 
				
			||||||
import net.sergeych.utools.pack
 | 
					import net.sergeych.utools.pack
 | 
				
			||||||
 | 
					
 | 
				
			||||||
private val idCounter = AtomicCounter(0)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * This class is not normally used directly. This is a local interface that supports
 | 
					 * This class is not normally used directly. This is a local interface that supports
 | 
				
			||||||
 * secure transport command layer (encrypted calls/results) to work with [KiloRemoteInterface].
 | 
					 * secure transport command layer (encrypted calls/results) to work with [KiloRemoteInterface].
 | 
				
			||||||
@ -25,10 +12,7 @@ private val idCounter = AtomicCounter(0)
 | 
				
			|||||||
internal class KiloL0Interface<T>(
 | 
					internal class KiloL0Interface<T>(
 | 
				
			||||||
    private val clientInterface: LocalInterface<KiloScope<T>>,
 | 
					    private val clientInterface: LocalInterface<KiloScope<T>>,
 | 
				
			||||||
    private val deferredParams: CompletableDeferred<KiloParams<T>>,
 | 
					    private val deferredParams: CompletableDeferred<KiloParams<T>>,
 | 
				
			||||||
) : LocalInterface<Unit>() {
 | 
					): LocalInterface<Unit>() {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    override var logTag: String = "KL0:${idCounter.incrementAndGet()}"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    init {
 | 
					    init {
 | 
				
			||||||
        // local interface uses the same session as a client:
 | 
					        // local interface uses the same session as a client:
 | 
				
			||||||
        addErrorProvider(clientInterface)
 | 
					        addErrorProvider(clientInterface)
 | 
				
			||||||
@ -43,10 +27,7 @@ internal class KiloL0Interface<T>(
 | 
				
			|||||||
                    0u,
 | 
					                    0u,
 | 
				
			||||||
                    clientInterface.execute(params.scope, call.name, call.serializedArgs)
 | 
					                    clientInterface.execute(params.scope, call.name, call.serializedArgs)
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            } catch(t: BreakConnectionException) {
 | 
					            } catch (t: Throwable) {
 | 
				
			||||||
                throw t
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            catch (t: Throwable) {
 | 
					 | 
				
			||||||
                clientInterface.encodeError(0u, t)
 | 
					                clientInterface.encodeError(0u, t)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            params.encrypt(pack(result))
 | 
					            params.encrypt(pack(result))
 | 
				
			||||||
 | 
				
			|||||||
@ -1,23 +1,23 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec
 | 
					package net.sergeych.kiloparsec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.ionspin.kotlin.crypto.keyexchange.KeyExchangeSessionKeyPair
 | 
				
			||||||
 | 
					import com.ionspin.kotlin.crypto.secretbox.SecretBox
 | 
				
			||||||
 | 
					import com.ionspin.kotlin.crypto.secretbox.crypto_secretbox_NONCEBYTES
 | 
				
			||||||
 | 
					import com.ionspin.kotlin.crypto.util.decodeFromUByteArray
 | 
				
			||||||
 | 
					import com.ionspin.kotlin.crypto.util.encodeToUByteArray
 | 
				
			||||||
import kotlinx.serialization.Serializable
 | 
					import kotlinx.serialization.Serializable
 | 
				
			||||||
import net.sergeych.bintools.toDataSource
 | 
					import net.sergeych.bintools.toDataSource
 | 
				
			||||||
import net.sergeych.bipack.BipackDecoder
 | 
					import net.sergeych.bipack.BipackDecoder
 | 
				
			||||||
import net.sergeych.bipack.Unsigned
 | 
					import net.sergeych.bipack.BipackEncoder
 | 
				
			||||||
import net.sergeych.crypto2.*
 | 
					import net.sergeych.crypto.DecryptionFailedException
 | 
				
			||||||
import net.sergeych.synctools.ProtectedOp
 | 
					import net.sergeych.crypto.Key
 | 
				
			||||||
import net.sergeych.synctools.invoke
 | 
					import net.sergeych.crypto.randomBytes
 | 
				
			||||||
 | 
					import net.sergeych.crypto.randomUInt
 | 
				
			||||||
 | 
					import net.sergeych.tools.ProtectedOp
 | 
				
			||||||
import net.sergeych.utools.pack
 | 
					import net.sergeych.utools.pack
 | 
				
			||||||
 | 
					import net.sergeych.utools.unpack
 | 
				
			||||||
 | 
					import org.komputing.khash.keccak.Keccak
 | 
				
			||||||
 | 
					import org.komputing.khash.keccak.KeccakParameter
 | 
				
			||||||
import kotlin.math.roundToInt
 | 
					import kotlin.math.roundToInt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@ -32,18 +32,23 @@ import kotlin.math.roundToInt
 | 
				
			|||||||
data class KiloParams<S>(
 | 
					data class KiloParams<S>(
 | 
				
			||||||
    val isServer: Boolean,
 | 
					    val isServer: Boolean,
 | 
				
			||||||
    val transport: RemoteInterface,
 | 
					    val transport: RemoteInterface,
 | 
				
			||||||
    val sessionKey: SafeKeyExchange.SessionKey,
 | 
					    val sessionKeyPair: KeyExchangeSessionKeyPair,
 | 
				
			||||||
    val scopeSession: S,
 | 
					    val scopeSession: S,
 | 
				
			||||||
    val remoteIdentity: VerifyingPublicKey?,
 | 
					    val remoteIdentity: Key.Verifying?,
 | 
				
			||||||
    val remoteTransport: RemoteInterface,
 | 
					    val remoteTransport: RemoteInterface
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
    @Serializable
 | 
					    @Serializable
 | 
				
			||||||
    data class Package(
 | 
					    data class Package(
 | 
				
			||||||
        @Unsigned
 | 
					 | 
				
			||||||
        val nonce: ULong,
 | 
					        val nonce: ULong,
 | 
				
			||||||
        val encryptedMessage: UByteArray,
 | 
					        val encryptedMessage: UByteArray,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Serializable
 | 
				
			||||||
 | 
					    data class FilledData(
 | 
				
			||||||
 | 
					        val message: UByteArray,
 | 
				
			||||||
 | 
					        val fill: UByteArray,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private var nonce = 0UL
 | 
					    private var nonce = 0UL
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val scope: KiloScope<S> by lazy {
 | 
					    val scope: KiloScope<S> by lazy {
 | 
				
			||||||
@ -51,39 +56,81 @@ data class KiloParams<S>(
 | 
				
			|||||||
            override val session = scopeSession
 | 
					            override val session = scopeSession
 | 
				
			||||||
            override val remote: RemoteInterface = remoteTransport
 | 
					            override val remote: RemoteInterface = remoteTransport
 | 
				
			||||||
            override val sessionToken: UByteArray = token
 | 
					            override val sessionToken: UByteArray = token
 | 
				
			||||||
            override val remoteIdentity: VerifyingPublicKey? = this@KiloParams.remoteIdentity
 | 
					            override val remoteIdentity: Key.Verifying? = this@KiloParams.remoteIdentity
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val token: UByteArray by lazy {
 | 
					    val token: UByteArray by lazy {
 | 
				
			||||||
        blake2b("token_".encodeToUByteArray() + sessionKey.sessionTag).sliceArray(0..<SymmetricKey.nonceLength)
 | 
					        val base = if (isServer) sessionKeyPair.sendKey + sessionKeyPair.receiveKey
 | 
				
			||||||
 | 
					        else sessionKeyPair.receiveKey + sessionKeyPair.sendKey
 | 
				
			||||||
 | 
					        Keccak.digest(
 | 
				
			||||||
 | 
					            base.toByteArray(), KeccakParameter.KECCAK_256
 | 
				
			||||||
 | 
					        ).toUByteArray().sliceArray(0..<crypto_secretbox_NONCEBYTES)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private val numericNonce = NumericNonce(token)
 | 
					    private val sendBase by lazy {
 | 
				
			||||||
 | 
					        Keccak.digest(
 | 
				
			||||||
 | 
					            sessionKeyPair.sendKey.toByteArray(), KeccakParameter.KECCAK_256
 | 
				
			||||||
 | 
					        ).toUByteArray().sliceArray(0..<crypto_secretbox_NONCEBYTES)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private val protectedOp = ProtectedOp()
 | 
					    private val receiveBase by lazy {
 | 
				
			||||||
 | 
					        Keccak.digest(
 | 
				
			||||||
 | 
					            sessionKeyPair.receiveKey.toByteArray(), KeccakParameter.KECCAK_256
 | 
				
			||||||
 | 
					        ).toUByteArray().sliceArray(0..<crypto_secretbox_NONCEBYTES)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private inline fun encodeNonce(base: UByteArray, nonce: ULong): UByteArray {
 | 
				
			||||||
 | 
					        val result = base.copyOf()
 | 
				
			||||||
 | 
					        var x = nonce
 | 
				
			||||||
 | 
					        var i = 0
 | 
				
			||||||
 | 
					        while (x > 0u) {
 | 
				
			||||||
 | 
					            result[i] = result[i] xor (x and 0xFFu).toUByte()
 | 
				
			||||||
 | 
					            x = x shr 8
 | 
				
			||||||
 | 
					            i++
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return result
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private inline fun encodeSendNonce(nonce: ULong): UByteArray = encodeNonce(sendBase, nonce)
 | 
				
			||||||
 | 
					    private inline fun encodeReceiveNonce(nonce: ULong): UByteArray = encodeNonce(receiveBase, nonce)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun encrypt(plainText: String): UByteArray = encrypt(plainText.encodeToUByteArray())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private val proptectedOp = ProtectedOp()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Encrypt using send keys and proper nonce
 | 
					     * Encrypt using send keys and proper nonce
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    fun encrypt(message: UByteArray, fillFactor: Float = 0f): UByteArray {
 | 
					    fun encrypt(message: UByteArray, fillFactor: Float = 0f): UByteArray {
 | 
				
			||||||
        val fill = if (fillFactor > 0f)
 | 
					        val fill: UByteArray = if (fillFactor > 0f)
 | 
				
			||||||
            0..(message.size * fillFactor).roundToInt()
 | 
					            randomBytes(randomUInt((message.size * fillFactor).roundToInt()))
 | 
				
			||||||
        else
 | 
					        else
 | 
				
			||||||
            null
 | 
					            ubyteArrayOf()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        val n = protectedOp.invoke { nonce++ }
 | 
					        val withFill = BipackEncoder.encode(FilledData(message, fill)).toUByteArray()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val n = proptectedOp { nonce++ }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return pack(
 | 
					        return pack(
 | 
				
			||||||
            Package(n, sessionKey.encryptWithNonce(message, numericNonce.withULong(n), fill))
 | 
					            Package(n, SecretBox.easy(withFill, encodeSendNonce(n), sessionKeyPair.sendKey))
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    //    fun decryptString(cipherText: UByteArray): String = decrypt(cipherText).decodeFromUByteArray()
 | 
					    fun decryptString(cipherText: UByteArray): String = decrypt(cipherText).decodeFromUByteArray()
 | 
				
			||||||
    fun decrypt(encryptedMessage: UByteArray): UByteArray =
 | 
					    fun decrypt(encryptedMessage: UByteArray): UByteArray {
 | 
				
			||||||
        protectDecryption {
 | 
					        val p: Package = BipackDecoder.decode(encryptedMessage.toDataSource())
 | 
				
			||||||
            val p: Package = BipackDecoder.decode(encryptedMessage.toDataSource())
 | 
					        try {
 | 
				
			||||||
            sessionKey.decryptWithNonce(p.encryptedMessage, numericNonce.withULong(p.nonce))
 | 
					            return unpack<FilledData>(
 | 
				
			||||||
 | 
					                SecretBox.openEasy(
 | 
				
			||||||
 | 
					                    p.encryptedMessage,
 | 
				
			||||||
 | 
					                    encodeReceiveNonce(p.nonce),
 | 
				
			||||||
 | 
					                    sessionKeyPair.receiveKey
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            ).message
 | 
				
			||||||
 | 
					        } catch (_: com.ionspin.kotlin.crypto.secretbox.SecretBoxCorruptedOrTamperedDataExceptionOrInvalidKey) {
 | 
				
			||||||
 | 
					            throw DecryptionFailedException()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -1,13 +1,3 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec
 | 
					package net.sergeych.kiloparsec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import kotlinx.coroutines.CompletableDeferred
 | 
					import kotlinx.coroutines.CompletableDeferred
 | 
				
			||||||
@ -17,13 +7,7 @@ import net.sergeych.utools.unpack
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
private var L1IdCounter = 0
 | 
					private var L1IdCounter = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					// todo: We don't need it deferred here
 | 
				
			||||||
 * Represents a remote interface for interacting with a Kiloparsec service.
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @param S the scope type of the Kiloparsec service which will be used to call local functions remotely
 | 
					 | 
				
			||||||
 * @property deferredParams a [CompletableDeferred] that resolves to the parameters required for making remote calls
 | 
					 | 
				
			||||||
 * @property clientInterface a [LocalInterface] used to communicate with the local client
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
class KiloRemoteInterface<S>(
 | 
					class KiloRemoteInterface<S>(
 | 
				
			||||||
    private val deferredParams: CompletableDeferred<KiloParams<S>>,
 | 
					    private val deferredParams: CompletableDeferred<KiloParams<S>>,
 | 
				
			||||||
    private val clientInterface: LocalInterface<KiloScope<S>>,
 | 
					    private val clientInterface: LocalInterface<KiloScope<S>>,
 | 
				
			||||||
@ -45,10 +29,5 @@ class KiloRemoteInterface<S>(
 | 
				
			|||||||
            else -> throw RemoteInterface.Exception("unexpected block type: $block")
 | 
					            else -> throw RemoteInterface.Exception("unexpected block type: $block")
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    override suspend fun <A> push(cmd: Command<A, Unit>, args: A) {
 | 
					 | 
				
			||||||
        val params = deferredParams.await()
 | 
					 | 
				
			||||||
        params.transport.call(L0Call, params.encrypt(cmd.packCall(args)))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,17 +1,6 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec
 | 
					package net.sergeych.kiloparsec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import net.sergeych.crypto2.SigningKey
 | 
					import net.sergeych.crypto.Key
 | 
				
			||||||
import net.sergeych.crypto2.VerifyingPublicKey
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Scope for Kiloparsec client/server commands execution, contain per-connection specific data. The scope
 | 
					 * Scope for Kiloparsec client/server commands execution, contain per-connection specific data. The scope
 | 
				
			||||||
@ -37,8 +26,8 @@ interface KiloScope<S> {
 | 
				
			|||||||
    val sessionToken: UByteArray
 | 
					    val sessionToken: UByteArray
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * If the remote part has provided a secret key, e.g., gave non-null [SigningKey] on construction,
 | 
					     * If the remote part has provided a secret key, e.g., gave non-null [Key.Signing] on construction,
 | 
				
			||||||
     * the kiloparsec checks it in the MITM-safe way and provides its [VerifyingPublicKey] shared key here.
 | 
					     * the kiloparsec checks it in the MITM-safe way and provides its [Key.Verifying] shared key here.
 | 
				
			||||||
     * Knowing a remote party shared key, it is possible to be sure that the connection is made directly
 | 
					     * Knowing a remote party shared key, it is possible to be sure that the connection is made directly
 | 
				
			||||||
     * to this party with no middle point intruders.
 | 
					     * to this party with no middle point intruders.
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
@ -48,6 +37,6 @@ interface KiloScope<S> {
 | 
				
			|||||||
     * In spite of the above said, which means, non-null value in this field means the key is authorized, but
 | 
					     * In spite of the above said, which means, non-null value in this field means the key is authorized, but
 | 
				
			||||||
     * It is up to the caller to ensure it is expected key of the remote party.
 | 
					     * It is up to the caller to ensure it is expected key of the remote party.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    val remoteIdentity: VerifyingPublicKey?
 | 
					    val remoteIdentity: Key.Verifying?
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,153 +0,0 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import kotlinx.coroutines.CancellationException
 | 
					 | 
				
			||||||
import kotlinx.coroutines.flow.Flow
 | 
					 | 
				
			||||||
import kotlinx.coroutines.launch
 | 
					 | 
				
			||||||
import net.sergeych.crypto2.SigningKey
 | 
					 | 
				
			||||||
import net.sergeych.kiloparsec.adapter.InetTransportDevice
 | 
					 | 
				
			||||||
import net.sergeych.mp_logger.LogTag
 | 
					 | 
				
			||||||
import net.sergeych.mp_logger.debug
 | 
					 | 
				
			||||||
import net.sergeych.mp_logger.exception
 | 
					 | 
				
			||||||
import net.sergeych.mp_logger.info
 | 
					 | 
				
			||||||
import net.sergeych.mp_tools.globalLaunch
 | 
					 | 
				
			||||||
import net.sergeych.tools.AtomicCounter
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
private val instances = AtomicCounter()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * The Kiloparsec server.
 | 
					 | 
				
			||||||
 * Server accepts incoming connections and serves them using the same [clientInterface].
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * ## Incoming connections
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * Server collecting incoming connections provided by [connections] `Flow`. For each incoming connection
 | 
					 | 
				
			||||||
 * the Kiloparsec handshake is performed, then the session object is created, see below, and connection is
 | 
					 | 
				
			||||||
 * served with [clientInterface] until closed.
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * ## Session param [S]
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * After the successful handshake server creates new session for each connection calling the [sessionBuilder].
 | 
					 | 
				
			||||||
 * Then it creates a [KiloScope] so [KiloScope.session] holds this connection-specific instance. Then
 | 
					 | 
				
			||||||
 * [KiloInterface.onConnected] is called with this scope. Session can be used to hold connection state. Session
 | 
					 | 
				
			||||||
 * objects are not persistent, but could be initialized in [KiloInterface.onConnected] where the remote side
 | 
					 | 
				
			||||||
 * [KiloScope.remoteIdentity] is already verified and set.
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * ## Usage:
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * Create a shared library between you server and clients, to specify the interface (otherwise you can
 | 
					 | 
				
			||||||
 * share sources).
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * Suppose we have session with a state:
 | 
					 | 
				
			||||||
 * ```kotlin
 | 
					 | 
				
			||||||
 * data class Session(
 | 
					 | 
				
			||||||
 *     var data: String,
 | 
					 | 
				
			||||||
 * )
 | 
					 | 
				
			||||||
 *```
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * And some commands to access and change it, in the shared library too:
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * ```kotlin
 | 
					 | 
				
			||||||
 * val cmdSave by command<String, Unit>()
 | 
					 | 
				
			||||||
 * val cmdLoad by command<Unit, String>()
 | 
					 | 
				
			||||||
 * val cmdDrop by command<Unit, Unit>()
 | 
					 | 
				
			||||||
 * val cmdException by command<Unit, Unit>()
 | 
					 | 
				
			||||||
 * ```
 | 
					 | 
				
			||||||
 * Then the server code (TCP/IP variant) could look like:
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * ```kotlin
 | 
					 | 
				
			||||||
 * // The server implementation (could be shared between server instances connected
 | 
					 | 
				
			||||||
 * // to different protocol adapters):
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * val cli = KiloInterface<Session>().apply {
 | 
					 | 
				
			||||||
 *     // Suppose we want to throw this exception at the caller site, so we need to register it:
 | 
					 | 
				
			||||||
 *     registerError { SomeException() }
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *     // Session initialization. If you need a sessino to depend initially on the client's identity.
 | 
					 | 
				
			||||||
 *     // you can do it here:
 | 
					 | 
				
			||||||
 *     onConnected {
 | 
					 | 
				
			||||||
 *         // check the remoteIdentity
 | 
					 | 
				
			||||||
 *         session.data = if( remoteIdentity == somePublicVerifyingKey )
 | 
					 | 
				
			||||||
 *              "known"
 | 
					 | 
				
			||||||
 *         else
 | 
					 | 
				
			||||||
 *              "unknown"
 | 
					 | 
				
			||||||
 *     }
 | 
					 | 
				
			||||||
 *     on(cmdSave) { session.data = it }
 | 
					 | 
				
			||||||
 *     on(cmdLoad) {
 | 
					 | 
				
			||||||
 *         session.data
 | 
					 | 
				
			||||||
 *     }
 | 
					 | 
				
			||||||
 *     on(cmdException) {
 | 
					 | 
				
			||||||
 *         throw TestException()
 | 
					 | 
				
			||||||
 *     }
 | 
					 | 
				
			||||||
 *     on(cmdDrop) {
 | 
					 | 
				
			||||||
 *         throw LocalInterface.BreakConnectionException()
 | 
					 | 
				
			||||||
 *     }
 | 
					 | 
				
			||||||
 * }
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * // Create the server instance that accepts incoming TCP/IP connections on all local interfaces using the
 | 
					 | 
				
			||||||
 * // specified port:
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * val server = KiloServer(cli, acceptTcpDevice(port)) {
 | 
					 | 
				
			||||||
 *     // This creates a new session
 | 
					 | 
				
			||||||
 *     Session("unknown")
 | 
					 | 
				
			||||||
 * }
 | 
					 | 
				
			||||||
 * ```
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * See [KiloClient] to connect to the server.
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @param S the type of the server session object, returned by [sessionBuilder]. See above
 | 
					 | 
				
			||||||
 * @param clientInterface the interface available for remote calls
 | 
					 | 
				
			||||||
 * @param connections flow of incoming connections. Server stops when the flow is fully collected (normally
 | 
					 | 
				
			||||||
 *          it shouldn't)
 | 
					 | 
				
			||||||
 * @param serverSecretKey the [SigningKey] used to identify this server during Kiloparsec handshake.
 | 
					 | 
				
			||||||
 * @param sessionBuilder callback that creates session objects for successful incoming connections
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
class KiloServer<S>(
 | 
					 | 
				
			||||||
    private val clientInterface: KiloInterface<S>,
 | 
					 | 
				
			||||||
    private val connections: Flow<InetTransportDevice>,
 | 
					 | 
				
			||||||
    private val serverSecretKey: SigningKey? = null,
 | 
					 | 
				
			||||||
    private val sessionBuilder: () -> S,
 | 
					 | 
				
			||||||
) : LogTag("KS:${instances.incrementAndGet()}") {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private val job = globalLaunch {
 | 
					 | 
				
			||||||
        connections.collect { device ->
 | 
					 | 
				
			||||||
            launch {
 | 
					 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                    info { "connected ${device}" }
 | 
					 | 
				
			||||||
                    KiloServerConnection(clientInterface, device, sessionBuilder(), serverSecretKey)
 | 
					 | 
				
			||||||
                        .apply { debug { "server connection is ready" } }
 | 
					 | 
				
			||||||
                        .run()
 | 
					 | 
				
			||||||
                } catch (_: CancellationException) {
 | 
					 | 
				
			||||||
                } catch (cce: LocalInterface.BreakConnectionException) {
 | 
					 | 
				
			||||||
                    info { "Closed exception caught, closing (${cce.flushSendQueue}" }
 | 
					 | 
				
			||||||
                } catch (t: Throwable) {
 | 
					 | 
				
			||||||
                    exception { "unexpected while creating kiloclient" to t }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Stop the server and cancel all pending sessions. Unlike finishing the flow passed
 | 
					 | 
				
			||||||
     * for [KiloServer.connections], it will cancel all currently active sessions.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    fun close() {
 | 
					 | 
				
			||||||
        job.cancel()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Server is closed either if [close] was called or all [KiloServer.connections] were collected and flow was
 | 
					 | 
				
			||||||
     * closed.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    @Suppress("unused")
 | 
					 | 
				
			||||||
    val isClosed: Boolean get() = job.isCompleted
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,18 +1,8 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec
 | 
					package net.sergeych.kiloparsec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.ionspin.kotlin.crypto.keyexchange.KeyExchange
 | 
				
			||||||
import kotlinx.coroutines.CompletableDeferred
 | 
					import kotlinx.coroutines.CompletableDeferred
 | 
				
			||||||
import net.sergeych.crypto2.SafeKeyExchange
 | 
					import net.sergeych.crypto.Key
 | 
				
			||||||
import net.sergeych.crypto2.SigningKey
 | 
					 | 
				
			||||||
import net.sergeych.mp_logger.LogTag
 | 
					import net.sergeych.mp_logger.LogTag
 | 
				
			||||||
import net.sergeych.mp_logger.Loggable
 | 
					import net.sergeych.mp_logger.Loggable
 | 
				
			||||||
import net.sergeych.mp_logger.debug
 | 
					import net.sergeych.mp_logger.debug
 | 
				
			||||||
@ -33,15 +23,15 @@ class KiloServerConnection<S>(
 | 
				
			|||||||
    private val clientInterface: KiloInterface<S>,
 | 
					    private val clientInterface: KiloInterface<S>,
 | 
				
			||||||
    private val device: Transport.Device,
 | 
					    private val device: Transport.Device,
 | 
				
			||||||
    private val session: S,
 | 
					    private val session: S,
 | 
				
			||||||
    private val serverSigningKey: SigningKey? = null
 | 
					    private val serverSigningKey: Key.Signing? = null
 | 
				
			||||||
) : RemoteInterface, Loggable by LogTag("SRV${++serverIds}") {
 | 
					) : RemoteInterface, Loggable by LogTag("SRV${++serverIds}") {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Shortcut to construct with [KiloConnectionData] intance
 | 
					     * Shortcut to construct with [KiloConnectionData] intance
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    @Suppress("unused")
 | 
					    @Suppress("unused")
 | 
				
			||||||
    constructor(localInterface: KiloInterface<S>, connection: KiloConnectionData<S>, serverSecretKey: SigningKey? = null)
 | 
					    constructor(localInterface: KiloInterface<S>, connection: KiloConnectionData<S>, serverSigningKey: Key.Signing? = null)
 | 
				
			||||||
            : this(localInterface, connection.device, connection.session, serverSecretKey)
 | 
					            : this(localInterface, connection.device, connection.session, serverSigningKey)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private val kiloRemoteInterface = CompletableDeferred<KiloRemoteInterface<S>>()
 | 
					    private val kiloRemoteInterface = CompletableDeferred<KiloRemoteInterface<S>>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -57,7 +47,9 @@ class KiloServerConnection<S>(
 | 
				
			|||||||
        val l0Interface = KiloL0Interface(clientInterface, deferredParams).apply {
 | 
					        val l0Interface = KiloL0Interface(clientInterface, deferredParams).apply {
 | 
				
			||||||
            var params: KiloParams<S>? = null
 | 
					            var params: KiloParams<S>? = null
 | 
				
			||||||
            on(L0Request) {
 | 
					            on(L0Request) {
 | 
				
			||||||
                val sk = pair.serverSessionKey(it.publicKey)
 | 
					                val sk = KeyExchange.serverSessionKeys(
 | 
				
			||||||
 | 
					                    pair.publicKey, pair.secretKey, it.publicKey
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                params = KiloParams(
 | 
					                params = KiloParams(
 | 
				
			||||||
                    true,
 | 
					                    true,
 | 
				
			||||||
@ -67,9 +59,15 @@ class KiloServerConnection<S>(
 | 
				
			|||||||
                    null,
 | 
					                    null,
 | 
				
			||||||
                    this@KiloServerConnection
 | 
					                    this@KiloServerConnection
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                Handshake(1u, pair.publicKey, serverSigningKey?.seal(params!!.token))
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                var verifying: Key.Verifying? = null
 | 
				
			||||||
 | 
					                var signature: UByteArray? = null
 | 
				
			||||||
 | 
					                if( serverSigningKey != null ) {
 | 
				
			||||||
 | 
					                    verifying = serverSigningKey.verifying
 | 
				
			||||||
 | 
					                    signature = serverSigningKey.sign(params!!.token)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                Handshake(1u, pair.publicKey, verifying, signature)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            on(L0ClientId) {
 | 
					            on(L0ClientId) {
 | 
				
			||||||
                var p = params ?: throw RemoteInterface.ClosedException("wrong handshake sequence")
 | 
					                var p = params ?: throw RemoteInterface.ClosedException("wrong handshake sequence")
 | 
				
			||||||
                val ci = unpack<ClientIdentity>(p.decrypt(it))
 | 
					                val ci = unpack<ClientIdentity>(p.decrypt(it))
 | 
				
			||||||
@ -85,27 +83,22 @@ class KiloServerConnection<S>(
 | 
				
			|||||||
                kiloRemoteInterface.complete(
 | 
					                kiloRemoteInterface.complete(
 | 
				
			||||||
                    KiloRemoteInterface(deferredParams, clientInterface)
 | 
					                    KiloRemoteInterface(deferredParams, clientInterface)
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                clientInterface.onConnectHandlers.invokeAll(p.scope)
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        val transport = Transport(device, l0Interface, Unit)
 | 
					        val transport = Transport(device, l0Interface, Unit)
 | 
				
			||||||
            deferredTransport.complete(transport)
 | 
					            deferredTransport.complete(transport)
 | 
				
			||||||
        kiloRemoteInterface.complete(KiloRemoteInterface(deferredParams,clientInterface))
 | 
					        kiloRemoteInterface.complete(KiloRemoteInterface(deferredParams,clientInterface))
 | 
				
			||||||
        debug { "starting the transport"}
 | 
					        debug { "starintg the transport"}
 | 
				
			||||||
        transport.run()
 | 
					        transport.run()
 | 
				
			||||||
        debug { "server transport finished" }
 | 
					        debug { "server transport finished" }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    companion object {
 | 
					    companion object {
 | 
				
			||||||
        val pair = SafeKeyExchange()
 | 
					        val pair = KeyExchange.keypair()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R {
 | 
					    override suspend fun <A, R> call(cmd: Command<A, R>, args: A): R {
 | 
				
			||||||
        return kiloRemoteInterface.await().call(cmd, args)
 | 
					        return kiloRemoteInterface.await().call(cmd, args)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    override suspend fun <A> push(cmd: Command<A, Unit>, args: A) {
 | 
					 | 
				
			||||||
        kiloRemoteInterface.await().push(cmd, args)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -1,51 +1,14 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec
 | 
					package net.sergeych.kiloparsec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import net.sergeych.mp_logger.LogTag
 | 
					 | 
				
			||||||
import net.sergeych.mp_logger.Loggable
 | 
					 | 
				
			||||||
import net.sergeych.mp_logger.exception
 | 
					 | 
				
			||||||
import net.sergeych.mp_logger.info
 | 
					 | 
				
			||||||
import net.sergeych.tools.AtomicCounter
 | 
					 | 
				
			||||||
import net.sergeych.utools.firstNonNull
 | 
					import net.sergeych.utools.firstNonNull
 | 
				
			||||||
import kotlin.reflect.KClass
 | 
					import kotlin.reflect.KClass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
private typealias RawCommandHandler<C> = suspend (C, UByteArray) -> UByteArray
 | 
					private typealias RawCommandHandler<C> = suspend (C, UByteArray) -> UByteArray
 | 
				
			||||||
 | 
					
 | 
				
			||||||
private val idCounter = AtomicCounter()
 | 
					open class LocalInterface<S> {
 | 
				
			||||||
 | 
					 | 
				
			||||||
open class LocalInterface<S> : Loggable by LogTag("LocalInterface${idCounter.incrementAndGet()}") {
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private val commands = mutableMapOf<String, RawCommandHandler<S>>()
 | 
					    private val commands = mutableMapOf<String, RawCommandHandler<S>>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Instruct the transport to immediately break the connection.
 | 
					 | 
				
			||||||
     * This exception is not passed to the remote end, instead, transport device breaks
 | 
					 | 
				
			||||||
     * connection to remote when receiving it.
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * Remote interface will throw [RemoteInterface.ClosedException] as the break will be detected. As reaction time
 | 
					 | 
				
			||||||
     * it depends on the transport in use, we recommend sending some registered exception first if you need
 | 
					 | 
				
			||||||
     * to pass important data, or implement special commands on both sides.
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * __Important note:__ _it is not allowed to throw [RemoteInterface.ClosedException] directly!_
 | 
					 | 
				
			||||||
     * This exception is processed internally and can't be sent over the network.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    open class BreakConnectionException(
 | 
					 | 
				
			||||||
        text: String = "break connection request",
 | 
					 | 
				
			||||||
        val flushSendQueue: Boolean = true,
 | 
					 | 
				
			||||||
    ) : RuntimeException(text) {
 | 
					 | 
				
			||||||
        override val message: String?
 | 
					 | 
				
			||||||
            get() = super.message + " (flush=$flushSendQueue)"
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * New session creator. Rarely needed directlym it can be used for delegation
 | 
					     * New session creator. Rarely needed directlym it can be used for delegation
 | 
				
			||||||
     * of local interfaces.
 | 
					     * of local interfaces.
 | 
				
			||||||
@ -77,18 +40,14 @@ open class LocalInterface<S> : Loggable by LogTag("LocalInterface${idCounter.inc
 | 
				
			|||||||
        name: String,
 | 
					        name: String,
 | 
				
			||||||
        packedArgs: UByteArray,
 | 
					        packedArgs: UByteArray,
 | 
				
			||||||
    ): UByteArray =
 | 
					    ): UByteArray =
 | 
				
			||||||
        (commands[name] ?: throw RemoteInterface.UnknownCommand(name))
 | 
					        (commands[name] ?: throw RemoteInterface.UnknownCommand())
 | 
				
			||||||
            .invoke(scope, packedArgs)
 | 
					            .invoke(scope, packedArgs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private val errorByClass = mutableMapOf<KClass<*>, String>()
 | 
					    private val errorByClass = mutableMapOf<KClass<*>, String>()
 | 
				
			||||||
    private val errorBuilder = mutableMapOf<String, (String, UByteArray?) -> Throwable>()
 | 
					    private val errorBuilder = mutableMapOf<String, (String, UByteArray?) -> Throwable>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    fun <T : Throwable> registerError(
 | 
				
			||||||
     * Register exception for automatic transmission over the kiloparsec connection using its [klass]
 | 
					 | 
				
			||||||
     * and possibly override code. In most cases it is simpler to use [registerError].
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    fun <T : Throwable> registerErrorClass(
 | 
					 | 
				
			||||||
        klass: KClass<T>, code: String = klass.simpleName!!,
 | 
					        klass: KClass<T>, code: String = klass.simpleName!!,
 | 
				
			||||||
        exceptionBuilder: (String, UByteArray?) -> T,
 | 
					        exceptionBuilder: (String, UByteArray?) -> T,
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
@ -96,17 +55,10 @@ open class LocalInterface<S> : Loggable by LogTag("LocalInterface${idCounter.inc
 | 
				
			|||||||
        errorBuilder[code] = exceptionBuilder
 | 
					        errorBuilder[code] = exceptionBuilder
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Register an exception to be transmitted automatically over the kiloparsec connection.
 | 
					 | 
				
			||||||
     * Example:
 | 
					 | 
				
			||||||
     * ```kotlin
 | 
					 | 
				
			||||||
     * localInterface.registerError { IllegalArgumentException(it) }
 | 
					 | 
				
			||||||
     * ```
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    inline fun <reified T : Throwable> registerError(
 | 
					    inline fun <reified T : Throwable> registerError(
 | 
				
			||||||
        noinline exceptionBuilder: (String) -> T,
 | 
					        noinline exceptionBuilder: (String) -> T,
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
        registerErrorClass(T::class) { msg, _ -> exceptionBuilder(msg) }
 | 
					        registerError(T::class) { msg, _ -> exceptionBuilder(msg) }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val errorProviders = mutableListOf<LocalInterface<*>>()
 | 
					    val errorProviders = mutableListOf<LocalInterface<*>>()
 | 
				
			||||||
@ -116,26 +68,18 @@ open class LocalInterface<S> : Loggable by LogTag("LocalInterface${idCounter.inc
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fun getErrorCode(t: Throwable): String? =
 | 
					    fun getErrorCode(t: Throwable): String? =
 | 
				
			||||||
        (t as? ExceptionWithCode)?.code
 | 
					        errorByClass[t::class] ?: errorProviders.firstNonNull { it.getErrorCode(t) }
 | 
				
			||||||
            ?: errorByClass[t::class] ?: errorProviders.firstNonNull { it.getErrorCode(t) }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fun encodeError(forId: UInt, t: Throwable): Transport.Block.Error =
 | 
					    fun encodeError(forId: UInt, t: Throwable): Transport.Block.Error =
 | 
				
			||||||
        if (t is RemoteInterface.ClosedException) {
 | 
					        getErrorCode(t)?.let { Transport.Block.Error(forId, it, t.message) }
 | 
				
			||||||
            exception { "Illegal attempt to send ClosedException" to t }
 | 
					            ?: Transport.Block.Error(forId, "UnknownError", t.message)
 | 
				
			||||||
            encodeError(forId, RemoteInterface.InternalError("TCE"))
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        else
 | 
					 | 
				
			||||||
            getErrorCode(t)?.let { Transport.Block.Error(forId, it, t.message) }
 | 
					 | 
				
			||||||
                ?: Transport.Block.Error(forId, "UnknownError", "${t::class.simpleName}: ${t.message}")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    open fun getErrorBuilder(code: String): ((String, UByteArray?) -> Throwable)? =
 | 
					    open fun getErrorBuilder(code: String): ((String, UByteArray?) -> Throwable)? =
 | 
				
			||||||
        errorBuilder[code] ?: errorProviders.firstNonNull { it.getErrorBuilder(code) }
 | 
					        errorBuilder[code] ?: errorProviders.firstNonNull { it.getErrorBuilder(code) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fun decodeError(tbe: Transport.Block.Error): Throwable =
 | 
					    fun decodeError(tbe: Transport.Block.Error): Throwable =
 | 
				
			||||||
        getErrorBuilder(tbe.code)?.invoke(tbe.message, tbe.extra)
 | 
					        getErrorBuilder(tbe.code)?.invoke(tbe.message, tbe.extra)
 | 
				
			||||||
            ?: RemoteInterface.RemoteException(tbe).also {
 | 
					            ?: RemoteInterface.RemoteException(tbe)
 | 
				
			||||||
                info { "can't decode error ${tbe.code}: ${tbe.message}" }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fun decodeAndThrow(tbe: Transport.Block.Error): Nothing {
 | 
					    fun decodeAndThrow(tbe: Transport.Block.Error): Nothing {
 | 
				
			||||||
        throw decodeError(tbe)
 | 
					        throw decodeError(tbe)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										12
									
								
								src/commonMain/kotlin/net/sergeych/kiloparsec/ProxyDevice.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/commonMain/kotlin/net/sergeych/kiloparsec/ProxyDevice.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					package net.sergeych.kiloparsec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import kotlinx.coroutines.channels.Channel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ProxyDevice(capacity: Int=1024) : Transport.Device {
 | 
				
			||||||
 | 
					    override val input = Channel<UByteArray?>(capacity)
 | 
				
			||||||
 | 
					    override val output= Channel<UByteArray?>(capacity)
 | 
				
			||||||
 | 
					    override suspend fun close() {
 | 
				
			||||||
 | 
					        runCatching { input.close() }
 | 
				
			||||||
 | 
					        runCatching { output.close() }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,13 +1,3 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec
 | 
					package net.sergeych.kiloparsec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@ -25,12 +15,11 @@ interface RemoteInterface {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Is thrown when the channel is closed, in an attempt to execute a command, also to all pending
 | 
					     * Is thrown when the channel is closed, in an attempt to execute a command, also to all pending
 | 
				
			||||||
     * calls (see [call]). Client code should never throw it. If command handler needs to break connection
 | 
					     * calls (see [call]).
 | 
				
			||||||
     * it should throw [LocalInterface.BreakConnectionException]
 | 
					 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    open class ClosedException(t: String = "connection is closed") : Exception(t)
 | 
					    open class ClosedException(t: String = "connection is closed") : Exception(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    open class SecurityException(t: String = "invalid remote id and signature") : LocalInterface.BreakConnectionException(t)
 | 
					    open class SecurityException(t: String = "invalid remote id and signature") : ClosedException(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    open class InvalidDataException(msg: String="invalid data, can't unpack") : Exception(msg)
 | 
					    open class InvalidDataException(msg: String="invalid data, can't unpack") : Exception(msg)
 | 
				
			||||||
@ -50,37 +39,12 @@ interface RemoteInterface {
 | 
				
			|||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Command is not supported by the remote party
 | 
					     * Command is not supported by the remote party
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    class UnknownCommand(commandName: String) : RemoteException("UnknownCommand: $commandName")
 | 
					    class UnknownCommand : RemoteException("UnknownCommand")
 | 
				
			||||||
 | 
					 | 
				
			||||||
    open class InternalError(code: String="0"): RemoteException("Internal error: $code")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    suspend fun <R> call(cmd: Command<Unit, R>): R = call(cmd, Unit)
 | 
					    suspend fun <R> call(cmd: Command<Unit, R>): R = call(cmd, Unit)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Call the remote procedure with specified [args] and return its result of type [R]. The calling coroutine
 | 
					     * Call the remote procedure with specified args and return its result
 | 
				
			||||||
     * suspends until the request is performed on the remote side and the result value (also `Unit`) will be received.
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * When it is not necessary to wait for the return value and/or command execution, it is recommended to
 | 
					 | 
				
			||||||
     * use [push] instead.
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     *  @throws RemoteException if the execution caused exception on the remote size
 | 
					 | 
				
			||||||
     *  @throws Exception for registered exceptions, see [LocalInterface.registerError], etc.
 | 
					 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    suspend fun <A, R> call(cmd: Command<A, R>, args: A): R
 | 
					    suspend fun <A, R> call(cmd: Command<A, R>, args: A): R
 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Push the notification without waiting for reception or processing.
 | 
					 | 
				
			||||||
     * It returns immediately after sending data to the transport (e.g., to the network).
 | 
					 | 
				
			||||||
     * Use [call] if it is necessary to wait until the command will be received and processed by the remote.
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * Push is onlu available for commands without returned value.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    suspend fun <A> push(cmd: Command<A, Unit>, args: A)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Push the command with no args.
 | 
					 | 
				
			||||||
     * It returns immediately after sending data to the transport (e.g., to the network).
 | 
					 | 
				
			||||||
     * Use [call] if it is necessary to wait until the command will be received and processed by the remote.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    suspend fun push(cmd: Command<Unit,Unit>) = push(cmd,Unit)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -1,18 +1,6 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec
 | 
					package net.sergeych.kiloparsec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import kotlinx.coroutines.*
 | 
					import kotlinx.coroutines.*
 | 
				
			||||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
 | 
					 | 
				
			||||||
import kotlinx.coroutines.channels.ClosedSendChannelException
 | 
					 | 
				
			||||||
import kotlinx.coroutines.channels.ReceiveChannel
 | 
					import kotlinx.coroutines.channels.ReceiveChannel
 | 
				
			||||||
import kotlinx.coroutines.channels.SendChannel
 | 
					import kotlinx.coroutines.channels.SendChannel
 | 
				
			||||||
import kotlinx.coroutines.sync.Mutex
 | 
					import kotlinx.coroutines.sync.Mutex
 | 
				
			||||||
@ -25,8 +13,7 @@ import kotlinx.serialization.descriptors.SerialDescriptor
 | 
				
			|||||||
import kotlinx.serialization.encoding.Decoder
 | 
					import kotlinx.serialization.encoding.Decoder
 | 
				
			||||||
import kotlinx.serialization.encoding.Encoder
 | 
					import kotlinx.serialization.encoding.Encoder
 | 
				
			||||||
import kotlinx.serialization.serializer
 | 
					import kotlinx.serialization.serializer
 | 
				
			||||||
import net.sergeych.bipack.Unsigned
 | 
					import net.sergeych.crypto.toDump
 | 
				
			||||||
import net.sergeych.crypto2.toDump
 | 
					 | 
				
			||||||
import net.sergeych.kiloparsec.Transport.Device
 | 
					import net.sergeych.kiloparsec.Transport.Device
 | 
				
			||||||
import net.sergeych.mp_logger.*
 | 
					import net.sergeych.mp_logger.*
 | 
				
			||||||
import net.sergeych.utools.pack
 | 
					import net.sergeych.utools.pack
 | 
				
			||||||
@ -54,7 +41,7 @@ class Transport<S>(
 | 
				
			|||||||
         * Input blocks. When the device is disconnected, it should send one null to this channel
 | 
					         * Input blocks. When the device is disconnected, it should send one null to this channel
 | 
				
			||||||
         * to notify the owner. When [close] is called, the channel should be closed.
 | 
					         * to notify the owner. When [close] is called, the channel should be closed.
 | 
				
			||||||
         */
 | 
					         */
 | 
				
			||||||
        val input: ReceiveChannel<UByteArray>
 | 
					        val input: ReceiveChannel<UByteArray?>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /**
 | 
					        /**
 | 
				
			||||||
         * Send a binary block to a remote party where it should be received and put into [input]
 | 
					         * Send a binary block to a remote party where it should be received and put into [input]
 | 
				
			||||||
@ -67,19 +54,12 @@ class Transport<S>(
 | 
				
			|||||||
         * possible. This method must not throw exceptions.
 | 
					         * possible. This method must not throw exceptions.
 | 
				
			||||||
         */
 | 
					         */
 | 
				
			||||||
        suspend fun close()
 | 
					        suspend fun close()
 | 
				
			||||||
 | 
					 | 
				
			||||||
        suspend fun flush() {}
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Serializable(TransportBlockSerializer::class)
 | 
					    @Serializable(TransportBlockSerializer::class)
 | 
				
			||||||
    sealed class Block {
 | 
					    sealed class Block {
 | 
				
			||||||
        @Serializable
 | 
					        @Serializable
 | 
				
			||||||
        data class Call(
 | 
					        data class Call(val id: UInt, val name: String, val packedArgs: UByteArray) : Block() {
 | 
				
			||||||
            @Unsigned
 | 
					 | 
				
			||||||
            val id: UInt,
 | 
					 | 
				
			||||||
            val name: String,
 | 
					 | 
				
			||||||
            val packedArgs: UByteArray
 | 
					 | 
				
			||||||
        ) : Block() {
 | 
					 | 
				
			||||||
            override fun equals(other: Any?): Boolean {
 | 
					            override fun equals(other: Any?): Boolean {
 | 
				
			||||||
                if (this === other) return true
 | 
					                if (this === other) return true
 | 
				
			||||||
                if (other !is Call) return false
 | 
					                if (other !is Call) return false
 | 
				
			||||||
@ -99,10 +79,10 @@ class Transport<S>(
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        @Serializable
 | 
					        @Serializable
 | 
				
			||||||
        data class Response(@Unsigned val forId: UInt, val packedResult: UByteArray) : Block()
 | 
					        data class Response(val forId: UInt, val packedResult: UByteArray) : Block()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        @Serializable
 | 
					        @Serializable
 | 
				
			||||||
        data class Error(@Unsigned val forId: UInt, val code: String, val text: String? = null, val extra: UByteArray? = null) :
 | 
					        data class Error(val forId: UInt, val code: String, val text: String? = null, val extra: UByteArray? = null) :
 | 
				
			||||||
            Block() {
 | 
					            Block() {
 | 
				
			||||||
            val message by lazy { text ?: "remote exception: $code" }
 | 
					            val message by lazy { text ?: "remote exception: $code" }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -114,8 +94,7 @@ class Transport<S>(
 | 
				
			|||||||
    var isClosed: Boolean = false
 | 
					    var isClosed: Boolean = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Send a call block for a command and packed args and return packed result if it is not an error. It suspends
 | 
					     * Send a call block for a command and packed args and return packed result if it is not an error
 | 
				
			||||||
     * until receiving answer from the remote side, even if returns `Unit`.
 | 
					 | 
				
			||||||
     * @throws RemoteInterface.RemoteException if the remote call caused an exception. Normally use [call] instead.
 | 
					     * @throws RemoteInterface.RemoteException if the remote call caused an exception. Normally use [call] instead.
 | 
				
			||||||
     * @throws RemoteInterface.ClosedException
 | 
					     * @throws RemoteInterface.ClosedException
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
@ -128,52 +107,18 @@ class Transport<S>(
 | 
				
			|||||||
        // We need to shield calls and lastID with mutex, but nothing more:
 | 
					        // We need to shield calls and lastID with mutex, but nothing more:
 | 
				
			||||||
        access.withLock {
 | 
					        access.withLock {
 | 
				
			||||||
            if (isClosed) throw RemoteInterface.ClosedException()
 | 
					            if (isClosed) throw RemoteInterface.ClosedException()
 | 
				
			||||||
            // the order is important: first id in use MUST BE >= 1, not zero:
 | 
					 | 
				
			||||||
            b = Block.Call(++lastId, name, packedArgs)
 | 
					            b = Block.Call(++lastId, name, packedArgs)
 | 
				
			||||||
            calls[b.id] = deferred
 | 
					            calls[b.id] = deferred
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // now we have mutex freed so we can call:
 | 
					        // now we have mutex freed so we can call:
 | 
				
			||||||
        val r = runCatching {
 | 
					        val r = device.output.trySend(pack(b).also { debug { ">>>\n${it.toDump()}" } })
 | 
				
			||||||
            do {
 | 
					        if (!r.isSuccess) deferred.completeExceptionally(RemoteInterface.ClosedException())
 | 
				
			||||||
                val cr = device.output.trySend(pack(b))
 | 
					 | 
				
			||||||
                if( cr.isClosed ) throw ClosedSendChannelException("can't send block: channel is closed")
 | 
					 | 
				
			||||||
                delay(100)
 | 
					 | 
				
			||||||
            } while(!cr.isSuccess)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (!r.isSuccess) {
 | 
					 | 
				
			||||||
            r.exceptionOrNull()?.let {
 | 
					 | 
				
			||||||
                exception { "failed to send output block" to it }
 | 
					 | 
				
			||||||
            } ?: run {
 | 
					 | 
				
			||||||
                error { "It should not happen: empty exception on block send failure" }
 | 
					 | 
				
			||||||
                throw RuntimeException("unexpected failure in sending block")
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            deferred.completeExceptionally(RemoteInterface.ClosedException())
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // it returns packed result or throws a proper error:
 | 
					        // it returns packed result or throws a proper error:
 | 
				
			||||||
        return deferred.await()
 | 
					        return deferred.await()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * Send a call block for a command and packed args and return packed result if it is not an error. It suspends
 | 
					 | 
				
			||||||
     * until receiving answer from the remote side, even if returns `Unit`.
 | 
					 | 
				
			||||||
     * @throws RemoteInterface.RemoteException if the remote call caused an exception. Normally use [call] instead.
 | 
					 | 
				
			||||||
     * @throws RemoteInterface.ClosedException
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    private suspend fun sendPushBlock(name: String, packedArgs: UByteArray) {
 | 
					 | 
				
			||||||
        if (isClosed) throw RemoteInterface.ClosedException()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // All push blocks have the same id == 0:
 | 
					 | 
				
			||||||
        val b = Block.Call(0u, name, packedArgs)
 | 
					 | 
				
			||||||
        val r = runCatching { device.output.send(pack(b).also { debug { ">>$\n${it.toDump()}" } }) }
 | 
					 | 
				
			||||||
        when(val e = r.exceptionOrNull()) {
 | 
					 | 
				
			||||||
            is RemoteInterface.ClosedException, is CancellationException, is RemoteInterface.RemoteException
 | 
					 | 
				
			||||||
                -> throw e
 | 
					 | 
				
			||||||
            else -> throw RemoteInterface.ClosedException()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Call the remote procedure with specified args and return its result
 | 
					     * Call the remote procedure with specified args and return its result
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
@ -182,10 +127,6 @@ class Transport<S>(
 | 
				
			|||||||
        return unpack(cmd.resultSerializer, result)
 | 
					        return unpack(cmd.resultSerializer, result)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override suspend fun <A>push(cmd: Command<A,Unit>,args: A) {
 | 
					 | 
				
			||||||
        sendPushBlock(cmd.name, pack(cmd.argsSerializer, args))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Start running the transport. This function suspends until the transport is closed
 | 
					     * Start running the transport. This function suspends until the transport is closed
 | 
				
			||||||
     * normally or by error. If you need to cancel it prematurely, cancel the coroutine
 | 
					     * normally or by error. If you need to cancel it prematurely, cancel the coroutine
 | 
				
			||||||
@ -196,96 +137,76 @@ class Transport<S>(
 | 
				
			|||||||
    suspend fun run() {
 | 
					    suspend fun run() {
 | 
				
			||||||
        coroutineScope {
 | 
					        coroutineScope {
 | 
				
			||||||
            debug { "awaiting incoming blocks" }
 | 
					            debug { "awaiting incoming blocks" }
 | 
				
			||||||
            // todo: rewrite it to close the job with no exceptions at all, always
 | 
					 | 
				
			||||||
            while (isActive && !isClosed) {
 | 
					            while (isActive && !isClosed) {
 | 
				
			||||||
                try {
 | 
					                try {
 | 
				
			||||||
                    device.input.receive().let { packed ->
 | 
					                    device.input.receive()?.let { packed ->
 | 
				
			||||||
 | 
					                        debug { "<<<\n${packed.toDump()}" }
 | 
				
			||||||
                        val b = unpack<Block>(packed)
 | 
					                        val b = unpack<Block>(packed)
 | 
				
			||||||
 | 
					                        debug { "<<$ $b" }
 | 
				
			||||||
 | 
					                        debug { "access state: ${access.isLocked}" }
 | 
				
			||||||
                        when (b) {
 | 
					                        when (b) {
 | 
				
			||||||
                            is Block.Error -> access.withLock {
 | 
					                            is Block.Error -> access.withLock {
 | 
				
			||||||
                                val error = localInterface.decodeError(b)
 | 
					                                val error = localInterface.decodeError(b)
 | 
				
			||||||
                                warning { "decoded error: ${error::class.simpleName}: $error" }
 | 
					                                warning { "decoded error: ${error::class.simpleName}: $error" }
 | 
				
			||||||
                                calls.remove(b.forId)?.completeExceptionally(localInterface.decodeError(b))
 | 
					                                calls.remove(b.forId)?.completeExceptionally(localInterface.decodeError(b))
 | 
				
			||||||
                                    ?: warning { "error handler not found for ${b.forId}" }
 | 
					                                    ?: warning { "error handler not found for ${b.forId}" }
 | 
				
			||||||
                                info { "error processed" }
 | 
					                                info { "error processed"}
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            is Block.Response -> access.withLock {
 | 
					                            is Block.Response -> access.withLock {
 | 
				
			||||||
                                calls.remove(b.forId)?.complete(b.packedResult)
 | 
					                                calls.remove(b.forId)?.let {
 | 
				
			||||||
 | 
					                                    debug { "activating wait handle for ${b.forId}" }
 | 
				
			||||||
 | 
					                                    it.complete(b.packedResult)
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
                                    ?: warning { "wait handle not found for ${b.forId}" }
 | 
					                                    ?: warning { "wait handle not found for ${b.forId}" }
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            is Block.Call -> launch {
 | 
					                            is Block.Call -> launch {
 | 
				
			||||||
                                try {
 | 
					                                try {
 | 
				
			||||||
                                    if (b.id == 0u)
 | 
					                                    send(
 | 
				
			||||||
                                        // Command does not waits return
 | 
					                                        Block.Response(
 | 
				
			||||||
                                        localInterface.execute(commandContext, b.name, b.packedArgs)
 | 
					                                            b.id,
 | 
				
			||||||
                                    else
 | 
					                                            localInterface.execute(commandContext, b.name, b.packedArgs)
 | 
				
			||||||
                                        send(
 | 
					 | 
				
			||||||
                                            Block.Response(
 | 
					 | 
				
			||||||
                                                b.id,
 | 
					 | 
				
			||||||
                                                localInterface.execute(commandContext, b.name, b.packedArgs)
 | 
					 | 
				
			||||||
                                            )
 | 
					 | 
				
			||||||
                                        )
 | 
					                                        )
 | 
				
			||||||
                                } catch (x: LocalInterface.BreakConnectionException) {
 | 
					                                    )
 | 
				
			||||||
                                    // handler forced close
 | 
					                                } catch (x: RemoteInterface.ClosedException) {
 | 
				
			||||||
                                    warning { "handler requested closing of the connection (${x.flushSendQueue}" }
 | 
					                                    // strange case: handler throws closed?
 | 
				
			||||||
                                    isClosed = true
 | 
					                                    error { "not supported: command handler for $b has thrown ClosedException" }
 | 
				
			||||||
                                    if (x.flushSendQueue) device.flush()
 | 
					                                    send(Block.Error(b.id, "UnexpectedException", x.message))
 | 
				
			||||||
                                    device.close()
 | 
					 | 
				
			||||||
                                } catch (x: RemoteInterface.RemoteException) {
 | 
					                                } catch (x: RemoteInterface.RemoteException) {
 | 
				
			||||||
                                    send(Block.Error(b.id, x.code, x.text, x.extra))
 | 
					                                    send(Block.Error(b.id, x.code, x.text, x.extra))
 | 
				
			||||||
                                } catch (t: Throwable) {
 | 
					                                } catch (t: Throwable) {
 | 
				
			||||||
                                    send(Block.Error(b.id, "UnknownError", t.message))
 | 
					                                    send(Block.Error(b.id, "UnknownError", t.message))
 | 
				
			||||||
                                }
 | 
					                                }
 | 
				
			||||||
 | 
					                                    .also { debug { "command executed: ${b.name}" } }
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					                    } ?: run {
 | 
				
			||||||
 | 
					                        debug { "remote channel close received" }
 | 
				
			||||||
 | 
					                        isClosed = true
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
//                    debug { "input step performed closed=$isClosed active=$isActive" }
 | 
					 | 
				
			||||||
                } catch (_: ClosedSendChannelException) {
 | 
					 | 
				
			||||||
                    info { "closed send channel" }
 | 
					 | 
				
			||||||
                    isClosed = true
 | 
					 | 
				
			||||||
                } catch (_: ClosedReceiveChannelException) {
 | 
					 | 
				
			||||||
                    info { "closed receive channel" }
 | 
					 | 
				
			||||||
                    isClosed = true
 | 
					 | 
				
			||||||
                } catch (cce: LocalInterface.BreakConnectionException) {
 | 
					 | 
				
			||||||
                    info { "closing connection by local request ($cce)" }
 | 
					 | 
				
			||||||
                    device.close()
 | 
					 | 
				
			||||||
                } catch (t: RemoteInterface.ClosedException) {
 | 
					 | 
				
			||||||
                    // it is ok: we just exit the coroutine normally
 | 
					 | 
				
			||||||
                    // and mark we're closing
 | 
					 | 
				
			||||||
                    isClosed = true
 | 
					 | 
				
			||||||
                } catch (_: CancellationException) {
 | 
					                } catch (_: CancellationException) {
 | 
				
			||||||
                    info { "loop is cancelled with CancellationException" }
 | 
					                    info { "loop is cancelled" }
 | 
				
			||||||
                    isClosed = true
 | 
					                    isClosed = true
 | 
				
			||||||
                } catch (t: Throwable) {
 | 
					                } catch (t: Throwable) {
 | 
				
			||||||
                    exception { "channel closed on error" to t }
 | 
					                    exception { "channel closed on error" to t }
 | 
				
			||||||
                    info { "isa? $isActive / $isClosed" }
 | 
					                    info { "isa? $isActive / $isClosed" }
 | 
				
			||||||
 | 
					                    runCatching { device.close() }
 | 
				
			||||||
                    isClosed = true
 | 
					                    isClosed = true
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            debug { "leaving transport loop" }
 | 
					 | 
				
			||||||
            access.withLock {
 | 
					            access.withLock {
 | 
				
			||||||
                debug { "access lock obtained" }
 | 
					 | 
				
			||||||
                isClosed = true
 | 
					                isClosed = true
 | 
				
			||||||
                debug { "closing device $device, calls in queue ${calls.size}" }
 | 
					                for (c in calls.values) c.completeExceptionally(RemoteInterface.ClosedException())
 | 
				
			||||||
                runCatching { device.close() }
 | 
					 | 
				
			||||||
                for (c in calls.values)
 | 
					 | 
				
			||||||
                    c.completeExceptionally(RemoteInterface.ClosedException())
 | 
					 | 
				
			||||||
                calls.clear()
 | 
					                calls.clear()
 | 
				
			||||||
                debug { "calls clear has been called" }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            debug { "no more active: $isActive / ${calls.size}" }
 | 
					            debug { "no more active: $isActive / ${calls.size}" }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        debug { "exiting transport loop" }
 | 
					        info { "exiting transport loop" }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private suspend fun send(block: Block) {
 | 
					    private suspend fun send(block: Block) {
 | 
				
			||||||
        try {
 | 
					        device.output.send(pack(block))
 | 
				
			||||||
            device.output.send(pack(block))
 | 
					 | 
				
			||||||
        } catch (_: ClosedSendChannelException) {
 | 
					 | 
				
			||||||
            throw RemoteInterface.ClosedException()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -314,7 +235,7 @@ object TransportBlockSerializer : KSerializer<Transport.Block> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun deserialize(decoder: Decoder): Transport.Block =
 | 
					    override fun deserialize(decoder: Decoder): Transport.Block =
 | 
				
			||||||
        when (val id = decoder.decodeByte().toInt()) {
 | 
					        when( val id = decoder.decodeByte().toInt()) {
 | 
				
			||||||
            0 -> decoder.decodeSerializableValue(serializer<Transport.Block.Call>())
 | 
					            0 -> decoder.decodeSerializableValue(serializer<Transport.Block.Call>())
 | 
				
			||||||
            1 -> decoder.decodeSerializableValue(serializer<Transport.Block.Error>())
 | 
					            1 -> decoder.decodeSerializableValue(serializer<Transport.Block.Error>())
 | 
				
			||||||
            2 -> decoder.decodeSerializableValue(serializer<Transport.Block.Response>())
 | 
					            2 -> decoder.decodeSerializableValue(serializer<Transport.Block.Response>())
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					package net.sergeych.kiloparsec.adapter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					internal data class ConnectionId(
 | 
				
			||||||
 | 
					    val address: NetworkAddress,
 | 
				
			||||||
 | 
					    val id: UInt,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
@ -0,0 +1,117 @@
 | 
				
			|||||||
 | 
					package net.sergeych.kiloparsec.adapter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import kotlinx.coroutines.*
 | 
				
			||||||
 | 
					import net.sergeych.crypto.Key
 | 
				
			||||||
 | 
					import net.sergeych.kiloparsec.KiloClientConnection
 | 
				
			||||||
 | 
					import net.sergeych.kiloparsec.KiloInterface
 | 
				
			||||||
 | 
					import net.sergeych.kiloparsec.ProxyDevice
 | 
				
			||||||
 | 
					import net.sergeych.mp_logger.*
 | 
				
			||||||
 | 
					import net.sergeych.mp_tools.globalLaunch
 | 
				
			||||||
 | 
					import net.sergeych.tools.AtomicCounter
 | 
				
			||||||
 | 
					import net.sergeych.utools.pack
 | 
				
			||||||
 | 
					import net.sergeych.utools.unpack
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private val instanceCounter = AtomicCounter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DatagramClient<S>(
 | 
				
			||||||
 | 
					    private val connector: DatagramConnector,
 | 
				
			||||||
 | 
					    val serverAddress: NetworkAddress,
 | 
				
			||||||
 | 
					    private val localInterface: KiloInterface<S>,
 | 
				
			||||||
 | 
					    private val clientSigningKey: Key.Signing? = null,
 | 
				
			||||||
 | 
					    private val createSession: () -> S,
 | 
				
			||||||
 | 
					) : LogTag("DGC:${instanceCounter.next()}") {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private var sessionId = 0u
 | 
				
			||||||
 | 
					    private var isClosed = false
 | 
				
			||||||
 | 
					    val job = globalLaunch {
 | 
				
			||||||
 | 
					        while (isActive && !isClosed) {
 | 
				
			||||||
 | 
					            openSession()
 | 
				
			||||||
 | 
					            pumpSession()
 | 
				
			||||||
 | 
					            debug { "closing session $sessionId" }
 | 
				
			||||||
 | 
					            sessionId = 0u
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private suspend fun pumpSession() {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            val proxyDevice = ProxyDevice()
 | 
				
			||||||
 | 
					            val client = KiloClientConnection<S>(localInterface, proxyDevice, createSession(), clientSigningKey)
 | 
				
			||||||
 | 
					            coroutineScope {
 | 
				
			||||||
 | 
					                fun stop() {
 | 
				
			||||||
 | 
					                    cancel()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                launch {
 | 
				
			||||||
 | 
					                    while (isActive) {
 | 
				
			||||||
 | 
					                        if (sessionId == 0u) {
 | 
				
			||||||
 | 
					                            error { "invalid zero sessionId in pump" }
 | 
				
			||||||
 | 
					                            stop()
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            val data = proxyDevice.output.receive()
 | 
				
			||||||
 | 
					                            if (data == null) {
 | 
				
			||||||
 | 
					                                warning { "kiloconnection send empty frame: closing" }
 | 
				
			||||||
 | 
					                                stop()
 | 
				
			||||||
 | 
					                            } else {
 | 
				
			||||||
 | 
					                                send(OuterPacket(sessionId, data))
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                while (isActive) {
 | 
				
			||||||
 | 
					                    val packet: OuterPacket = unpack(connector.incoming.receive().message)
 | 
				
			||||||
 | 
					                    if (packet.id == 0u) {
 | 
				
			||||||
 | 
					                        val result: DatagramResult = unpack(packet.payload)
 | 
				
			||||||
 | 
					                        if (result is DatagramResult.Error) {
 | 
				
			||||||
 | 
					                            if (result.error == DatagramError.Closed) {
 | 
				
			||||||
 | 
					                                info { "connection is closed on the remote part" }
 | 
				
			||||||
 | 
					                            } else {
 | 
				
			||||||
 | 
					                                warning { "unexpected error while pumping: ${result.error}" }
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            warning { "unexpected result: $result" }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        stop()
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        proxyDevice.input.send(packet.payload)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            proxyDevice.close()
 | 
				
			||||||
 | 
					        } catch (x: CancellationException) {
 | 
				
			||||||
 | 
					            throw x
 | 
				
			||||||
 | 
					        } catch (x: Exception) {
 | 
				
			||||||
 | 
					            x.printStackTrace()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private suspend fun openSession() {
 | 
				
			||||||
 | 
					        while (!isClosed) {
 | 
				
			||||||
 | 
					            send(OuterPacket(0u, ubyteArrayOf()))
 | 
				
			||||||
 | 
					            val packet: OuterPacket = unpack(connector.incoming.receive().message)
 | 
				
			||||||
 | 
					            if (packet.id == 0u) {
 | 
				
			||||||
 | 
					                when (val result: DatagramResult = unpack(packet.payload)) {
 | 
				
			||||||
 | 
					                    is DatagramResult.Error -> {
 | 
				
			||||||
 | 
					                        warning { "error opening connection: ${result.error}" }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    is DatagramResult.Ok -> {
 | 
				
			||||||
 | 
					                        debug { "connection established, our id is ${result.id}" }
 | 
				
			||||||
 | 
					                        sessionId = result.id
 | 
				
			||||||
 | 
					                        break
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                warning { "received unexpected openSession id: ${packet.id}" }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            delay(500)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private suspend fun send(packet: OuterPacket) {
 | 
				
			||||||
 | 
					        connector.send(pack(packet), serverAddress)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun close() {
 | 
				
			||||||
 | 
					        job.cancel()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					package net.sergeych.kiloparsec.adapter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum class DatagramError {
 | 
				
			||||||
 | 
					    Closed,
 | 
				
			||||||
 | 
					    NotExists,
 | 
				
			||||||
 | 
					    Busy
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					package net.sergeych.kiloparsec.adapter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import kotlinx.serialization.SerialName
 | 
				
			||||||
 | 
					import kotlinx.serialization.Serializable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					sealed class DatagramResult {
 | 
				
			||||||
 | 
					    @Serializable
 | 
				
			||||||
 | 
					    @SerialName("ok")
 | 
				
			||||||
 | 
					    data class Ok(val id: UInt) : DatagramResult()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Serializable
 | 
				
			||||||
 | 
					    @SerialName("err")
 | 
				
			||||||
 | 
					    data class Error(val error: DatagramError) : DatagramResult()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,136 @@
 | 
				
			|||||||
 | 
					package net.sergeych.kiloparsec.adapter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import kotlinx.coroutines.channels.ClosedReceiveChannelException
 | 
				
			||||||
 | 
					import kotlinx.coroutines.isActive
 | 
				
			||||||
 | 
					import kotlinx.coroutines.sync.Mutex
 | 
				
			||||||
 | 
					import kotlinx.coroutines.sync.withLock
 | 
				
			||||||
 | 
					import kotlinx.datetime.Instant
 | 
				
			||||||
 | 
					import net.sergeych.crypto.Key
 | 
				
			||||||
 | 
					import net.sergeych.kiloparsec.KiloInterface
 | 
				
			||||||
 | 
					import net.sergeych.kiloparsec.KiloServerConnection
 | 
				
			||||||
 | 
					import net.sergeych.kiloparsec.ProxyDevice
 | 
				
			||||||
 | 
					import net.sergeych.mp_logger.LogTag
 | 
				
			||||||
 | 
					import net.sergeych.mp_logger.debug
 | 
				
			||||||
 | 
					import net.sergeych.mp_tools.globalLaunch
 | 
				
			||||||
 | 
					import net.sergeych.tools.AtomicCounter
 | 
				
			||||||
 | 
					import net.sergeych.utools.now
 | 
				
			||||||
 | 
					import net.sergeych.utools.pack
 | 
				
			||||||
 | 
					import net.sergeych.utools.unpack
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private val instanceCounter = AtomicCounter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private class Connection<S>(
 | 
				
			||||||
 | 
					    val server: DatagramServer<S>,
 | 
				
			||||||
 | 
					    val connection: KiloServerConnection<S>,
 | 
				
			||||||
 | 
					    val device: ProxyDevice,
 | 
				
			||||||
 | 
					    val connectionId: ConnectionId,
 | 
				
			||||||
 | 
					    var lastActiveAt: Instant = now(),
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    suspend fun proceedFrame(frame: OuterPacket) {
 | 
				
			||||||
 | 
					        lastActiveAt = now()
 | 
				
			||||||
 | 
					        if (frame.payload.isEmpty()) {
 | 
				
			||||||
 | 
					            // ping
 | 
				
			||||||
 | 
					            server.sendData(connectionId,ubyteArrayOf())
 | 
				
			||||||
 | 
					        } else
 | 
				
			||||||
 | 
					            device.input.send(frame.payload)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private val job = globalLaunch {
 | 
				
			||||||
 | 
					        while (isActive) {
 | 
				
			||||||
 | 
					            val x = device.output.receive() ?: break
 | 
				
			||||||
 | 
					            server.sendData(connectionId, x)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    suspend fun close() {
 | 
				
			||||||
 | 
					        job.cancel()
 | 
				
			||||||
 | 
					        server.remove(connectionId)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DatagramServer<S>(
 | 
				
			||||||
 | 
					    private val connector: DatagramConnector,
 | 
				
			||||||
 | 
					    private val localInterface: KiloInterface<S>,
 | 
				
			||||||
 | 
					    private val serverSigningKey: Key.Signing? = null,
 | 
				
			||||||
 | 
					    private val createSession: () -> S,
 | 
				
			||||||
 | 
					) : LogTag("DGS:${instanceCounter.next()}") {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private val job = globalLaunch {
 | 
				
			||||||
 | 
					        while (isActive) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                val datagram = connector.incoming.receive()
 | 
				
			||||||
 | 
					                val outer = unpack<OuterPacket>(datagram.message)
 | 
				
			||||||
 | 
					                if (outer.id == 0u)
 | 
				
			||||||
 | 
					                    setupNewConnection(datagram.address, outer)
 | 
				
			||||||
 | 
					                else
 | 
				
			||||||
 | 
					                    proceedExistingConnection(datagram.address, outer)
 | 
				
			||||||
 | 
					            } catch (_: ClosedReceiveChannelException) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private val access = Mutex()
 | 
				
			||||||
 | 
					    private val connections = mutableMapOf<ConnectionId, Connection<S>>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun findFreeId(address: NetworkAddress): UInt {
 | 
				
			||||||
 | 
					        val existingIds = connections.keys.filter { it.address == address }.map { it.id }.toSet()
 | 
				
			||||||
 | 
					        debug { "existing connections from $address: $existingIds" }
 | 
				
			||||||
 | 
					        var i = 0u
 | 
				
			||||||
 | 
					        while (i in existingIds) i++
 | 
				
			||||||
 | 
					        debug { "found the free id: $i" }
 | 
				
			||||||
 | 
					        return i
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private suspend fun setupNewConnection(address: NetworkAddress, packet: OuterPacket) {
 | 
				
			||||||
 | 
					        val id = access.withLock {
 | 
				
			||||||
 | 
					            val connectionId = ConnectionId(address, findFreeId(address))
 | 
				
			||||||
 | 
					            val proxy = ProxyDevice()
 | 
				
			||||||
 | 
					            connections[connectionId] = Connection(
 | 
				
			||||||
 | 
					                this,
 | 
				
			||||||
 | 
					                KiloServerConnection(localInterface, proxy, createSession(), serverSigningKey),
 | 
				
			||||||
 | 
					                proxy,
 | 
				
			||||||
 | 
					                connectionId,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            connectionId
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        sendResult(id,DatagramResult.Ok(id.id))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private suspend fun proceedExistingConnection(address: NetworkAddress, outerPacket: OuterPacket) {
 | 
				
			||||||
 | 
					        val connectionId = ConnectionId(address, outerPacket.id)
 | 
				
			||||||
 | 
					        val c = access.withLock { connections[connectionId] }
 | 
				
			||||||
 | 
					        if (c == null) {
 | 
				
			||||||
 | 
					            sendResult(connectionId, DatagramResult.Error(DatagramError.NotExists))
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            c.proceedFrame(outerPacket)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Important. Closing the server also closes the connector which is supposed to be "owned"
 | 
				
			||||||
 | 
					     * exclusively by `DatagramServer`
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    fun close() {
 | 
				
			||||||
 | 
					        job.cancel()
 | 
				
			||||||
 | 
					        runCatching {
 | 
				
			||||||
 | 
					            connector.close()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    internal suspend fun remove(connectionId: ConnectionId) {
 | 
				
			||||||
 | 
					        access.withLock {
 | 
				
			||||||
 | 
					            connections.remove(connectionId)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    internal suspend fun sendData(connectionId: ConnectionId,data: UByteArray) {
 | 
				
			||||||
 | 
					        connector.send(pack(OuterPacket(connectionId.id, data)), connectionId.address)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    internal suspend fun sendResult(connectionId: ConnectionId, result: DatagramResult) {
 | 
				
			||||||
 | 
					        connector.send(pack(OuterPacket(0u, pack(result))), connectionId.address)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,27 +0,0 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec.adapter
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import kotlinx.coroutines.channels.Channel
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Transport device for inet protocol family with graceful shutdown on [close]
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
@Suppress("unused")
 | 
					 | 
				
			||||||
class InetTransportDevice(
 | 
					 | 
				
			||||||
    inputChannel: Channel<UByteArray>,
 | 
					 | 
				
			||||||
    outputChannel: Channel<UByteArray>,
 | 
					 | 
				
			||||||
    val remoteAddress: NetworkAddress,
 | 
					 | 
				
			||||||
    doClose: (suspend ()->Unit)? = null,
 | 
					 | 
				
			||||||
    doFlush: (suspend ()->Unit)? = null,
 | 
					 | 
				
			||||||
) : ProxyDevice(inputChannel, outputChannel, doClose, doFlush) {
 | 
					 | 
				
			||||||
    override fun toString(): String = "@$remoteAddress"
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,23 +1,51 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec.adapter
 | 
					package net.sergeych.kiloparsec.adapter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import kotlinx.coroutines.channels.ReceiveChannel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Multiplatform internet address.
 | 
					 * Multiplatform implementation of an internet address.
 | 
				
			||||||
 | 
					 * Notice to implementors. It must provide correct and effective [equals] and [hashCode].
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
data class NetworkAddress(
 | 
					interface NetworkAddress {
 | 
				
			||||||
    val host: String,
 | 
					    val host: String
 | 
				
			||||||
    val port: Int
 | 
					    val port: Int
 | 
				
			||||||
) {
 | 
					 | 
				
			||||||
    override fun toString(): String {
 | 
					 | 
				
			||||||
        return "$host:$port"
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Multiplatform datagram abstraction
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					interface Datagram {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Received message
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    val message: UByteArray
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Address from where the message was sent
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    val address: NetworkAddress
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Send a datagram in response, e.g., to the [address].
 | 
				
			||||||
 | 
					     * This method is optimized per single per-datagram use. If you need to send many datagram, use [DatagramConnector].
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    suspend fun respondWith(message: UByteArray)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@OptIn(ExperimentalStdlibApi::class)
 | 
				
			||||||
 | 
					interface DatagramConnector: AutoCloseable {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    val incoming: ReceiveChannel<Datagram>
 | 
				
			||||||
 | 
					    suspend fun send(message: UByteArray, networkAddress: NetworkAddress)
 | 
				
			||||||
 | 
					    @Suppress("unused")
 | 
				
			||||||
 | 
					    suspend fun send(message: UByteArray, datagramAddress: String) {
 | 
				
			||||||
 | 
					        send(message, networkAddressOf(datagramAddress))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    suspend fun send(message: UByteArray,host: String,port: Int) =
 | 
				
			||||||
 | 
					        send(message, NetworkAddress(host,port))
 | 
				
			||||||
 | 
					    override fun close()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					expect fun networkAddressOf(address: String): NetworkAddress
 | 
				
			||||||
 | 
					expect fun NetworkAddress(host: String,port: Int): NetworkAddress
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					package net.sergeych.kiloparsec.adapter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import kotlinx.serialization.Serializable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					internal data class OuterPacket(
 | 
				
			||||||
 | 
					    val id: UInt,
 | 
				
			||||||
 | 
					    val payload: UByteArray,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
@ -1,50 +0,0 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec.adapter
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import kotlinx.coroutines.channels.Channel
 | 
					 | 
				
			||||||
import kotlinx.coroutines.channels.ReceiveChannel
 | 
					 | 
				
			||||||
import kotlinx.coroutines.channels.SendChannel
 | 
					 | 
				
			||||||
import kotlinx.coroutines.delay
 | 
					 | 
				
			||||||
import net.sergeych.kiloparsec.Transport
 | 
					 | 
				
			||||||
import net.sergeych.tools.AtomicCounter
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
private val counter = AtomicCounter()
 | 
					 | 
				
			||||||
open class ProxyDevice(
 | 
					 | 
				
			||||||
    private val inputChannel: Channel<UByteArray>,
 | 
					 | 
				
			||||||
    private val outputChannel: Channel<UByteArray>,
 | 
					 | 
				
			||||||
    private val doClose: (suspend ()->Unit)? = null,
 | 
					 | 
				
			||||||
    private val doFlush: (suspend ()->Unit)? = null,
 | 
					 | 
				
			||||||
): Transport.Device {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    override val input: ReceiveChannel<UByteArray> = inputChannel
 | 
					 | 
				
			||||||
    override val output: SendChannel<UByteArray> = outputChannel
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    override suspend fun close() {
 | 
					 | 
				
			||||||
        kotlin.runCatching {  doClose?.invoke() }
 | 
					 | 
				
			||||||
        runCatching { inputChannel.close() }
 | 
					 | 
				
			||||||
        runCatching { outputChannel.close() }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    override suspend fun flush() {
 | 
					 | 
				
			||||||
        doFlush?.invoke()
 | 
					 | 
				
			||||||
        var cnt = 10
 | 
					 | 
				
			||||||
        while(!outputChannel.isEmpty) {
 | 
					 | 
				
			||||||
            if (cnt-- < 0) break
 | 
					 | 
				
			||||||
            delay(50)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        super.flush()
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private val id = counter.incrementAndGet()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    override fun toString(): String = "PX$id"
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,159 +0,0 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec.adapter
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import io.ktor.client.*
 | 
					 | 
				
			||||||
import io.ktor.client.plugins.websocket.*
 | 
					 | 
				
			||||||
import io.ktor.http.*
 | 
					 | 
				
			||||||
import io.ktor.websocket.*
 | 
					 | 
				
			||||||
import kotlinx.coroutines.CancellationException
 | 
					 | 
				
			||||||
import kotlinx.coroutines.CompletableDeferred
 | 
					 | 
				
			||||||
import kotlinx.coroutines.channels.Channel
 | 
					 | 
				
			||||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
 | 
					 | 
				
			||||||
import kotlinx.coroutines.channels.ClosedSendChannelException
 | 
					 | 
				
			||||||
import kotlinx.coroutines.launch
 | 
					 | 
				
			||||||
import kotlinx.io.IOException
 | 
					 | 
				
			||||||
import net.sergeych.crypto2.SigningKey
 | 
					 | 
				
			||||||
import net.sergeych.kiloparsec.*
 | 
					 | 
				
			||||||
import net.sergeych.mp_logger.*
 | 
					 | 
				
			||||||
import net.sergeych.mp_tools.decodeBase64Compact
 | 
					 | 
				
			||||||
import net.sergeych.mp_tools.encodeToBase64Compact
 | 
					 | 
				
			||||||
import net.sergeych.mp_tools.globalLaunch
 | 
					 | 
				
			||||||
import net.sergeych.tools.AtomicCounter
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
private val counter = AtomicCounter()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Shortcut to create websocket client. Use [websocketTransportDevice] with [KiloClient]
 | 
					 | 
				
			||||||
 * for fine-grained control.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
fun <S> websocketClient(
 | 
					 | 
				
			||||||
    path: String,
 | 
					 | 
				
			||||||
    clientInterface: KiloInterface<S> = KiloInterface(),
 | 
					 | 
				
			||||||
    secretKey: SigningKey? = null,
 | 
					 | 
				
			||||||
    useTextFrames: Boolean = false,
 | 
					 | 
				
			||||||
    sessionMaker: () -> S = {
 | 
					 | 
				
			||||||
        @Suppress("UNCHECKED_CAST")
 | 
					 | 
				
			||||||
        Unit as S
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
): KiloClient<S> {
 | 
					 | 
				
			||||||
    return KiloClient(clientInterface, secretKey) {
 | 
					 | 
				
			||||||
        KiloConnectionData(websocketTransportDevice(path, useTextFrames), sessionMaker())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Create kilopaarsec transport over websocket (ws or wss).
 | 
					 | 
				
			||||||
 * @param path websocket path (must start with ws:// or wss:// and contain a path part)
 | 
					 | 
				
			||||||
 * @client use default [HttpClient], it installs [WebSockets] plugin
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
fun websocketTransportDevice(
 | 
					 | 
				
			||||||
    path: String,
 | 
					 | 
				
			||||||
    useTextFrames: Boolean = false,
 | 
					 | 
				
			||||||
    client: HttpClient = HttpClient {
 | 
					 | 
				
			||||||
        install(WebSockets)
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
): Transport.Device {
 | 
					 | 
				
			||||||
    var u = Url(path)
 | 
					 | 
				
			||||||
    if (u.encodedPath.length <= 1)
 | 
					 | 
				
			||||||
        u = URLBuilder(u).apply {
 | 
					 | 
				
			||||||
            encodedPath = "/kp"
 | 
					 | 
				
			||||||
        }.build()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    val input = Channel<UByteArray>()
 | 
					 | 
				
			||||||
    val output = Channel<UByteArray>()
 | 
					 | 
				
			||||||
    val closeHandle = CompletableDeferred<Boolean>()
 | 
					 | 
				
			||||||
    val readyHandle = CompletableDeferred<Unit>()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    globalLaunch {
 | 
					 | 
				
			||||||
        val log = LogTag("KC:${counter.incrementAndGet()}")
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
            client.webSocket({
 | 
					 | 
				
			||||||
                url.protocol = u.protocol
 | 
					 | 
				
			||||||
                url.host = u.host
 | 
					 | 
				
			||||||
                url.port = u.port
 | 
					 | 
				
			||||||
                url.encodedPath = u.encodedPath
 | 
					 | 
				
			||||||
                url.parameters.appendAll(u.parameters)
 | 
					 | 
				
			||||||
                log.info { "kiloparsec server URL: $url" }
 | 
					 | 
				
			||||||
            }) {
 | 
					 | 
				
			||||||
                log.info { "connected to the server" }
 | 
					 | 
				
			||||||
//                println("SENDING!!!")
 | 
					 | 
				
			||||||
//                send("Helluva")
 | 
					 | 
				
			||||||
                readyHandle.complete(Unit)
 | 
					 | 
				
			||||||
                launch {
 | 
					 | 
				
			||||||
                    try {
 | 
					 | 
				
			||||||
                        for (block in output) {
 | 
					 | 
				
			||||||
                            if (useTextFrames)
 | 
					 | 
				
			||||||
                                send(
 | 
					 | 
				
			||||||
                                    Frame.Text(block.asByteArray().encodeToBase64Compact())
 | 
					 | 
				
			||||||
                                )
 | 
					 | 
				
			||||||
                            else
 | 
					 | 
				
			||||||
                                send(block.toByteArray())
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        log.info { "input is closed, closing the websocket" }
 | 
					 | 
				
			||||||
                        if (closeHandle.isActive) closeHandle.complete(true)
 | 
					 | 
				
			||||||
                    } catch (_: ClosedSendChannelException) {
 | 
					 | 
				
			||||||
                        log.info { "send channel closed" }
 | 
					 | 
				
			||||||
                    } catch (_: CancellationException) {
 | 
					 | 
				
			||||||
                    } catch (t: Throwable) {
 | 
					 | 
				
			||||||
                        log.info { "unexpected exception in websock sender: ${t.stackTraceToString()}" }
 | 
					 | 
				
			||||||
                        closeHandle.completeExceptionally(t)
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    if (closeHandle.isActive) closeHandle.complete(false)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                launch {
 | 
					 | 
				
			||||||
                    try {
 | 
					 | 
				
			||||||
                        for (f in incoming) {
 | 
					 | 
				
			||||||
                            when (f) {
 | 
					 | 
				
			||||||
                                is Frame.Binary -> input.send(f.readBytes().toUByteArray())
 | 
					 | 
				
			||||||
                                is Frame.Text -> input.send(f.readText().decodeBase64Compact().toUByteArray())
 | 
					 | 
				
			||||||
                                else -> log.warning { "ignoring unexpected frame of type ${f.frameType}" }
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        if (closeHandle.isActive) closeHandle.complete(true)
 | 
					 | 
				
			||||||
                    } catch (_: CancellationException) {
 | 
					 | 
				
			||||||
                        if (closeHandle.isActive) closeHandle.complete(false)
 | 
					 | 
				
			||||||
                    } catch (_: ClosedReceiveChannelException) {
 | 
					 | 
				
			||||||
                        log.warning { "receive channel closed unexpectedly" }
 | 
					 | 
				
			||||||
                        if (closeHandle.isActive) closeHandle.complete(false)
 | 
					 | 
				
			||||||
                    } catch (t: Throwable) {
 | 
					 | 
				
			||||||
                        log.exception { "unexpected error" to t }
 | 
					 | 
				
			||||||
                        if (closeHandle.isActive) closeHandle.complete(false)
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                if (!closeHandle.await()) {
 | 
					 | 
				
			||||||
                    log.warning { "Client is closing with error" }
 | 
					 | 
				
			||||||
                    throw RemoteInterface.ClosedException()
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                runCatching { output.close() }
 | 
					 | 
				
			||||||
                runCatching { input.close() }
 | 
					 | 
				
			||||||
                runCatching { close() }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } catch (x: IOException) {
 | 
					 | 
				
			||||||
            if ("refused" in x.toString()) log.debug { "connection refused" }
 | 
					 | 
				
			||||||
            else log.warning { "unexpected IO error $x" }
 | 
					 | 
				
			||||||
            runCatching { output.close() }
 | 
					 | 
				
			||||||
            runCatching { input.close() }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        log.info { "closing connection" }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    // Wait for connection be established or failed
 | 
					 | 
				
			||||||
    val device = ProxyDevice(input, output, doClose = {
 | 
					 | 
				
			||||||
        // we need to explicitly close the coroutine job, or it can hang for a long time
 | 
					 | 
				
			||||||
        // leaking resources.
 | 
					 | 
				
			||||||
        runCatching { output.close() }
 | 
					 | 
				
			||||||
        runCatching { input.close() }
 | 
					 | 
				
			||||||
        closeHandle.complete(true)
 | 
					 | 
				
			||||||
//            job.cancel()
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    return device
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@ -1,27 +1,16 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec
 | 
					package net.sergeych.kiloparsec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import kotlinx.serialization.Serializable
 | 
					import kotlinx.serialization.Serializable
 | 
				
			||||||
import net.sergeych.crypto2.SafeKeyExchange
 | 
					import net.sergeych.crypto.Key
 | 
				
			||||||
import net.sergeych.crypto2.Seal
 | 
					 | 
				
			||||||
import net.sergeych.crypto2.VerifyingPublicKey
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// L0 commands - key exchange and check:
 | 
					// L0 commands - key exchange and check:
 | 
				
			||||||
@Serializable
 | 
					@Serializable
 | 
				
			||||||
data class Handshake(val version: UInt, val publicKey: SafeKeyExchange.PublicKey,
 | 
					data class Handshake(val version: UInt, val publicKey: UByteArray,
 | 
				
			||||||
                     val signature: Seal? = null)
 | 
					                     val serverSharedKey: Key.Verifying? = null,
 | 
				
			||||||
 | 
					                     val signature: UByteArray? = null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Serializable
 | 
					@Serializable
 | 
				
			||||||
data class ClientIdentity(val clientIdKey: VerifyingPublicKey?, val signature: UByteArray?)
 | 
					data class ClientIdentity(val clientIdKey: Key.Verifying?, val signature: UByteArray?)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Level 0 command: request key exchange
 | 
					// Level 0 command: request key exchange
 | 
				
			||||||
internal val L0Request by command<Handshake, Handshake>()
 | 
					internal val L0Request by command<Handshake, Handshake>()
 | 
				
			||||||
 | 
				
			|||||||
@ -1,34 +0,0 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package net.sergeych.kiloparsec
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import kotlinx.coroutines.sync.Mutex
 | 
					 | 
				
			||||||
import kotlinx.coroutines.sync.withLock
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fun String.encodeToUByteArray() =
 | 
					 | 
				
			||||||
    encodeToByteArray().toUByteArray()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class SyncValue<T>(initialValue: T) {
 | 
					 | 
				
			||||||
    private val access = Mutex()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    var value = initialValue
 | 
					 | 
				
			||||||
    private set
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    suspend fun mutate(f: suspend (T)->T): T = access.withLock { f(value).also { value = it } }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Suppress("unused")
 | 
					 | 
				
			||||||
    suspend fun getAndSet(newValue: T): T = mutate {
 | 
					 | 
				
			||||||
        val old = value
 | 
					 | 
				
			||||||
        value = newValue
 | 
					 | 
				
			||||||
        old
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					package net.sergeych.tools
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AtomicCounter(startWith: Long=0) {
 | 
				
			||||||
 | 
					    private var op = ProtectedOp()
 | 
				
			||||||
 | 
					    private var  counter = startWith
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun next(): Long = op { counter++ }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										21
									
								
								src/commonMain/kotlin/net/sergeych/tools/ProtectedOp.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/commonMain/kotlin/net/sergeych/tools/ProtectedOp.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					package net.sergeych.tools
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Multiplatform interface to perform a regular (not suspend) operation
 | 
				
			||||||
 | 
					 * protected by a platform mutex (where necessary). Get real implementation
 | 
				
			||||||
 | 
					 * with [ProtectedOp]
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					interface ProtectedOpImplementation {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Call [f] iin mutually exclusive mode, it means that only one invocation
 | 
				
			||||||
 | 
					     * can be active at a time, all the rest are waiting until the current operation
 | 
				
			||||||
 | 
					     * will finish.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    operator fun <T>invoke(f: ()->T): T
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Get the platform-depended implementation of a mutex-protected operation.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					expect fun ProtectedOp(): ProtectedOpImplementation
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/commonMain/kotlin/net/sergeych/utools/collections.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/commonMain/kotlin/net/sergeych/utools/collections.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					package net.sergeych.utools
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Scan the collection and return the first non-null result of the [predicate] on it.
 | 
				
			||||||
 | 
					 * If all the elements give null with predicate call, returns null.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Note that collection is scanned only to the first non-null predicate result.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					fun <T,R>Collection<T>.firstNonNull(predicate: (T)->R?): R? {
 | 
				
			||||||
 | 
					    for( x in this ) predicate(x)?.let { return it }
 | 
				
			||||||
 | 
					    return null
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										46
									
								
								src/commonMain/kotlin/net/sergeych/utools/packing.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/commonMain/kotlin/net/sergeych/utools/packing.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					package net.sergeych.utools
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import kotlinx.serialization.KSerializer
 | 
				
			||||||
 | 
					import kotlinx.serialization.serializer
 | 
				
			||||||
 | 
					import net.sergeych.bintools.toDataSource
 | 
				
			||||||
 | 
					import net.sergeych.bipack.BipackDecoder
 | 
				
			||||||
 | 
					import net.sergeych.bipack.BipackEncoder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Effectively pack anyk nullable object. The result could be effectively packed
 | 
				
			||||||
 | 
					 * in turn as a part of a more complex structure.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * To avoid packing non-null mark,
 | 
				
			||||||
 | 
					 * we use a zero-size array, which, if in turn encoded, packs into a single
 | 
				
			||||||
 | 
					 * zero byte. Thus, we avoid extra byte spending for unnecessary null
 | 
				
			||||||
 | 
					 * check.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					inline fun <reified T> pack(element: T?): UByteArray = pack(serializer<T>(), element)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Unpack nullable data packed with [pack]
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					inline fun <reified T: Any?> unpack(encoded: UByteArray): T =
 | 
				
			||||||
 | 
					    unpack(serializer<T>(), encoded)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Effectively pack anyk nullable object. The result could be effectively packed
 | 
				
			||||||
 | 
					 * in turn as a part of a more complex structure.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * To avoid packing non-null mark,
 | 
				
			||||||
 | 
					 * we use a zero-size array, which, if in turn encoded, packs into a single
 | 
				
			||||||
 | 
					 * zero byte. Thus, we avoid extra byte spending for unnecessary null
 | 
				
			||||||
 | 
					 * check.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					fun <T>pack(serializer: KSerializer<T>, element: T?): UByteArray =
 | 
				
			||||||
 | 
					    if (element == null) ubyteArrayOf()
 | 
				
			||||||
 | 
					    else BipackEncoder.encode(serializer,element).toUByteArray()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Unpack nullable data packed with [pack]
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					@Suppress("UNCHECKED_CAST")
 | 
				
			||||||
 | 
					fun <T: Any?> unpack(serializer: KSerializer<T>, encoded: UByteArray): T =
 | 
				
			||||||
 | 
					    if (encoded.isEmpty()) null as T
 | 
				
			||||||
 | 
					    else BipackDecoder.decode(encoded.toByteArray().toDataSource(),serializer)
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/commonMain/kotlin/net/sergeych/utools/time.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/commonMain/kotlin/net/sergeych/utools/time.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					@file:Suppress("unused")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package net.sergeych.utools
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import kotlinx.datetime.Clock
 | 
				
			||||||
 | 
					import kotlinx.datetime.Instant
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun now(): Instant = Clock.System.now()
 | 
				
			||||||
 | 
					fun nowToSeconds(): Instant = Clock.System.now().truncateToSeconds()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fun Instant.truncateToSeconds(): Instant =
 | 
				
			||||||
 | 
					    Instant.fromEpochSeconds(toEpochMilliseconds()/1000)
 | 
				
			||||||
							
								
								
									
										183
									
								
								src/commonMain/kotlin/org/komputing/khash/keccak/Keccak.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								src/commonMain/kotlin/org/komputing/khash/keccak/Keccak.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,183 @@
 | 
				
			|||||||
 | 
					package org.komputing.khash.keccak
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.ionspin.kotlin.bignum.integer.BigInteger
 | 
				
			||||||
 | 
					import org.komputing.khash.keccak.extensions.fillWith
 | 
				
			||||||
 | 
					import kotlin.math.min
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					object Keccak {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private val BIT_65 = BigInteger.ONE shl (64)
 | 
				
			||||||
 | 
					    private val MAX_64_BITS = BIT_65 - BigInteger.ONE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun digest(value: ByteArray, parameter: KeccakParameter): ByteArray {
 | 
				
			||||||
 | 
					        val uState = IntArray(200)
 | 
				
			||||||
 | 
					        val uMessage = convertToUInt(value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var blockSize = 0
 | 
				
			||||||
 | 
					        var inputOffset = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Absorbing phase
 | 
				
			||||||
 | 
					        while (inputOffset < uMessage.size) {
 | 
				
			||||||
 | 
					            blockSize = min(uMessage.size - inputOffset, parameter.rateInBytes)
 | 
				
			||||||
 | 
					            for (i in 0 until blockSize) {
 | 
				
			||||||
 | 
					                uState[i] = uState[i] xor uMessage[i + inputOffset]
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            inputOffset += blockSize
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (blockSize == parameter.rateInBytes) {
 | 
				
			||||||
 | 
					                doF(uState)
 | 
				
			||||||
 | 
					                blockSize = 0
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Padding phase
 | 
				
			||||||
 | 
					        uState[blockSize] = uState[blockSize] xor parameter.d
 | 
				
			||||||
 | 
					        if (parameter.d and 0x80 != 0 && blockSize == parameter.rateInBytes - 1) {
 | 
				
			||||||
 | 
					            doF(uState)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        uState[parameter.rateInBytes - 1] = uState[parameter.rateInBytes - 1] xor 0x80
 | 
				
			||||||
 | 
					        doF(uState)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Squeezing phase
 | 
				
			||||||
 | 
					        val byteResults = mutableListOf<Byte>()
 | 
				
			||||||
 | 
					        var tOutputLen = parameter.outputLengthInBytes
 | 
				
			||||||
 | 
					        while (tOutputLen > 0) {
 | 
				
			||||||
 | 
					            blockSize = min(tOutputLen, parameter.rateInBytes)
 | 
				
			||||||
 | 
					            for (i in 0 until blockSize) {
 | 
				
			||||||
 | 
					                byteResults.add(uState[i].toByte().toInt().toByte())
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            tOutputLen -= blockSize
 | 
				
			||||||
 | 
					            if (tOutputLen > 0) {
 | 
				
			||||||
 | 
					                doF(uState)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return byteResults.toByteArray()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun doF(uState: IntArray) {
 | 
				
			||||||
 | 
					        val lState = Array(5) { Array(5) { BigInteger.ZERO } }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (i in 0..4) {
 | 
				
			||||||
 | 
					            for (j in 0..4) {
 | 
				
			||||||
 | 
					                val data = IntArray(8)
 | 
				
			||||||
 | 
					                val index = 8 * (i + 5 * j)
 | 
				
			||||||
 | 
					                uState.copyInto(data, 0, index, index + data.size)
 | 
				
			||||||
 | 
					                lState[i][j] = convertFromLittleEndianTo64(data)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        roundB(lState)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        uState.fillWith(0)
 | 
				
			||||||
 | 
					        for (i in 0..4) {
 | 
				
			||||||
 | 
					            for (j in 0..4) {
 | 
				
			||||||
 | 
					                val data = convertFrom64ToLittleEndian(lState[i][j])
 | 
				
			||||||
 | 
					                data.copyInto(uState, 8 * (i + 5 * j))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Permutation on the given state.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private fun roundB(state: Array<Array<BigInteger>>) {
 | 
				
			||||||
 | 
					        var lfsrState = 1
 | 
				
			||||||
 | 
					        for (round in 0..23) {
 | 
				
			||||||
 | 
					            val c = arrayOfNulls<BigInteger>(5)
 | 
				
			||||||
 | 
					            val d = arrayOfNulls<BigInteger>(5)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // θ step
 | 
				
			||||||
 | 
					            for (i in 0..4) {
 | 
				
			||||||
 | 
					                c[i] = state[i][0].xor(state[i][1]).xor(state[i][2]).xor(state[i][3]).xor(state[i][4])
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for (i in 0..4) {
 | 
				
			||||||
 | 
					                d[i] = c[(i + 4) % 5]!!.xor(c[(i + 1) % 5]!!.leftRotate64(1))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for (i in 0..4) {
 | 
				
			||||||
 | 
					                for (j in 0..4) {
 | 
				
			||||||
 | 
					                    state[i][j] = state[i][j].xor(d[i]!!)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // ρ and π steps
 | 
				
			||||||
 | 
					            var x = 1
 | 
				
			||||||
 | 
					            var y = 0
 | 
				
			||||||
 | 
					            var current = state[x][y]
 | 
				
			||||||
 | 
					            for (i in 0..23) {
 | 
				
			||||||
 | 
					                val tX = x
 | 
				
			||||||
 | 
					                x = y
 | 
				
			||||||
 | 
					                y = (2 * tX + 3 * y) % 5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                val shiftValue = current
 | 
				
			||||||
 | 
					                current = state[x][y]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                state[x][y] = shiftValue.leftRotate64Safely((i + 1) * (i + 2) / 2)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // χ step
 | 
				
			||||||
 | 
					            for (j in 0..4) {
 | 
				
			||||||
 | 
					                val t = arrayOfNulls<BigInteger>(5)
 | 
				
			||||||
 | 
					                for (i in 0..4) {
 | 
				
			||||||
 | 
					                    t[i] = state[i][j]
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                for (i in 0..4) {
 | 
				
			||||||
 | 
					                    // ~t[(i + 1) % 5]
 | 
				
			||||||
 | 
					                    val invertVal = t[(i + 1) % 5]!!.xor(MAX_64_BITS)
 | 
				
			||||||
 | 
					                    // t[i] ^ ((~t[(i + 1) % 5]) & t[(i + 2) % 5])
 | 
				
			||||||
 | 
					                    state[i][j] = t[i]!!.xor(invertVal.and(t[(i + 2) % 5]!!))
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // ι step
 | 
				
			||||||
 | 
					            for (i in 0..6) {
 | 
				
			||||||
 | 
					                lfsrState = (lfsrState shl 1 xor (lfsrState shr 7) * 0x71) % 256
 | 
				
			||||||
 | 
					                // pow(2, i) - 1
 | 
				
			||||||
 | 
					                val bitPosition = (1 shl i) - 1
 | 
				
			||||||
 | 
					                if (lfsrState and 2 != 0) {
 | 
				
			||||||
 | 
					                    state[0][0] = state[0][0].xor(BigInteger.ONE shl bitPosition)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Converts the given [data] array to an [IntArray] containing UInt values.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private fun convertToUInt(data: ByteArray) = IntArray(data.size) {
 | 
				
			||||||
 | 
					        data[it].toInt() and 0xFF
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Converts the given [data] array containing the little endian representation of a number to a [BigInteger].
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private fun convertFromLittleEndianTo64(data: IntArray): BigInteger {
 | 
				
			||||||
 | 
					        val value = data.map { it.toString(16) }
 | 
				
			||||||
 | 
					            .map { if (it.length == 2) it else "0$it" }
 | 
				
			||||||
 | 
					            .reversed()
 | 
				
			||||||
 | 
					            .joinToString("")
 | 
				
			||||||
 | 
					        return BigInteger.parseString(value, 16)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Converts the given [BigInteger] to a little endian representation as an [IntArray].
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private fun convertFrom64ToLittleEndian(uLong: BigInteger): IntArray {
 | 
				
			||||||
 | 
					        val asHex = uLong.toString(16)
 | 
				
			||||||
 | 
					        val asHexPadded = "0".repeat((8 * 2) - asHex.length) + asHex
 | 
				
			||||||
 | 
					        return IntArray(8) {
 | 
				
			||||||
 | 
					            ((7 - it) * 2).let { pos ->
 | 
				
			||||||
 | 
					                asHexPadded.substring(pos, pos + 2).toInt(16)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun BigInteger.leftRotate64Safely(rotate: Int) = leftRotate64(rotate % 64)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun BigInteger.leftRotate64(rotate: Int) = (this shr (64 - rotate)).add(this shl rotate).mod(BIT_65)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					@file:Suppress("unused")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package org.komputing.khash.keccak
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Parameters defining the FIPS 202 standard.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					enum class KeccakParameter(val rateInBytes: Int,val outputLengthInBytes: Int, val d: Int) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    KECCAK_224(144, 28, 0x01),
 | 
				
			||||||
 | 
					    KECCAK_256(136, 32, 0x01),
 | 
				
			||||||
 | 
					    KECCAK_384(104, 48, 0x01),
 | 
				
			||||||
 | 
					    KECCAK_512(72, 64, 0x01),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    SHA3_224(144, 28, 0x06),
 | 
				
			||||||
 | 
					    SHA3_256(136, 32, 0x06),
 | 
				
			||||||
 | 
					    SHA3_384(104, 48, 0x06),
 | 
				
			||||||
 | 
					    SHA3_512(72, 64, 0x06),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    SHAKE128(168, 32, 0x1F),
 | 
				
			||||||
 | 
					    SHAKE256(136, 64, 0x1F)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					package org.komputing.khash.keccak.extensions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Assigns the specified int value to each element of the specified
 | 
				
			||||||
 | 
					 * range in the specified array of ints. The range to be filled
 | 
				
			||||||
 | 
					 * extends from index <tt>fromIndex</tt>, inclusive, to index
 | 
				
			||||||
 | 
					 * <tt>toIndex</tt>, exclusive.  (If <tt>fromIndex==toIndex</tt>, the
 | 
				
			||||||
 | 
					 * range to be filled is empty.)
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param fromIndex the index of the first element (inclusive) to be
 | 
				
			||||||
 | 
					 *        filled with the specified value
 | 
				
			||||||
 | 
					 * @param toIndex the index of the last element (exclusive) to be
 | 
				
			||||||
 | 
					 *        filled with the specified value
 | 
				
			||||||
 | 
					 * @param value the value to be stored in all elements of the array
 | 
				
			||||||
 | 
					 * @throws IllegalArgumentException if <tt>fromIndex > toIndex</tt>
 | 
				
			||||||
 | 
					 * @throws ArrayIndexOutOfBoundsException if <tt>fromIndex < 0</tt> or
 | 
				
			||||||
 | 
					 *         <tt>toIndex > a.length</tt>
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					internal fun IntArray.fillWith(value: Int, fromIndex: Int = 0, toIndex: Int = this.size) {
 | 
				
			||||||
 | 
					    if (fromIndex > toIndex) {
 | 
				
			||||||
 | 
					        throw IllegalArgumentException(
 | 
				
			||||||
 | 
					            "fromIndex($fromIndex) > toIndex($toIndex)"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (fromIndex < 0) {
 | 
				
			||||||
 | 
					        throw ArrayIndexOutOfBoundsException(fromIndex)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (toIndex > this.size) {
 | 
				
			||||||
 | 
					        throw ArrayIndexOutOfBoundsException(toIndex)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (i in fromIndex until toIndex)
 | 
				
			||||||
 | 
					        this[i] = value
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Constructs a new [ArrayIndexOutOfBoundsException]
 | 
				
			||||||
 | 
					 * class with an argument indicating the illegal index.
 | 
				
			||||||
 | 
					 * @param index the illegal index.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					internal class ArrayIndexOutOfBoundsException(index: Int) : Throwable("Array index out of range: $index")
 | 
				
			||||||
@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					@file:Suppress("unused")
 | 
				
			||||||
 | 
					package org.komputing.khash.keccak.extensions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.komputing.khash.keccak.Keccak
 | 
				
			||||||
 | 
					import org.komputing.khash.keccak.KeccakParameter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Computes the proper Keccak digest of [this] byte array based on the given [parameter]
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					fun ByteArray.digestKeccak(parameter: KeccakParameter): ByteArray {
 | 
				
			||||||
 | 
					    return Keccak.digest(this, parameter)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Computes the proper Keccak digest of [this] string based on the given [parameter]
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					fun String.digestKeccak(parameter: KeccakParameter): ByteArray {
 | 
				
			||||||
 | 
					    return Keccak.digest(encodeToByteArray(), parameter)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,19 +1,8 @@
 | 
				
			|||||||
/*
 | 
					import com.ionspin.kotlin.crypto.secretbox.SecretBox
 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					import com.ionspin.kotlin.crypto.util.decodeFromUByteArray
 | 
				
			||||||
 *
 | 
					import com.ionspin.kotlin.crypto.util.encodeToUByteArray
 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import kotlinx.coroutines.test.runTest
 | 
					import kotlinx.coroutines.test.runTest
 | 
				
			||||||
import net.sergeych.crypto2.IllegalSignatureException
 | 
					import net.sergeych.crypto.*
 | 
				
			||||||
import net.sergeych.crypto2.SealedBox
 | 
					 | 
				
			||||||
import net.sergeych.crypto2.SigningSecretKey
 | 
					 | 
				
			||||||
import net.sergeych.crypto2.initCrypto
 | 
					 | 
				
			||||||
import net.sergeych.kiloparsec.encodeToUByteArray
 | 
					 | 
				
			||||||
import net.sergeych.utools.pack
 | 
					import net.sergeych.utools.pack
 | 
				
			||||||
import net.sergeych.utools.unpack
 | 
					import net.sergeych.utools.unpack
 | 
				
			||||||
import kotlin.test.*
 | 
					import kotlin.test.*
 | 
				
			||||||
@ -22,36 +11,47 @@ class KeysTest {
 | 
				
			|||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun testCreationAndMap() = runTest {
 | 
					    fun testCreationAndMap() = runTest {
 | 
				
			||||||
        initCrypto()
 | 
					        initCrypto()
 | 
				
			||||||
        val (stk,pbk) = SigningSecretKey.generatePair()
 | 
					        val (stk,pbk) = Key.Signing.pair()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        val x = mapOf( stk to "STK!", pbk to "PBK!")
 | 
					        val x = mapOf( stk to "STK!", pbk to "PBK!")
 | 
				
			||||||
        assertEquals("STK!", x[stk])
 | 
					        assertEquals("STK!", x[stk])
 | 
				
			||||||
        val s1 = SigningSecretKey(stk.keyBytes)
 | 
					        val s1 = Key.Signing(stk.packed)
 | 
				
			||||||
        assertEquals(stk, s1)
 | 
					        assertEquals(stk, s1)
 | 
				
			||||||
        assertEquals("STK!", x[s1])
 | 
					        assertEquals("STK!", x[s1])
 | 
				
			||||||
        assertEquals("PBK!", x[pbk])
 | 
					        assertEquals("PBK!", x[pbk])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        val data = "8 rays dev!".encodeToUByteArray()
 | 
					        val data = "8 rays dev!".encodeToUByteArray()
 | 
				
			||||||
        val data1 = "8 rays dev!".encodeToUByteArray()
 | 
					        val data1 = "8 rays dev!".encodeToUByteArray()
 | 
				
			||||||
        val s = stk.seal(data)
 | 
					        val s = SignedBox.Seal.create(stk, data)
 | 
				
			||||||
        assertTrue(s.isValid(data))
 | 
					        assertTrue(s.verify(data))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        data1[0] = 0x01u
 | 
					        data1[0] = 0x01u
 | 
				
			||||||
        assertFalse(s.isValid(data1))
 | 
					        assertFalse(s.verify(data1))
 | 
				
			||||||
        val p2 = SigningSecretKey.generatePair()
 | 
					        val p2 = Key.Signing.pair()
 | 
				
			||||||
        val p3 = SigningSecretKey.generatePair()
 | 
					        val p3 = Key.Signing.pair()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        val ms = SealedBox(data, s1) + p2.secretKey
 | 
					        val ms = SignedBox(data, s1) + p2.signing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // non tampered:
 | 
					        // non tampered:
 | 
				
			||||||
        val ms1 = unpack<SealedBox>(pack(ms))
 | 
					        val ms1 = unpack<SignedBox>(pack(ms))
 | 
				
			||||||
        assertContentEquals(data, ms1.message)
 | 
					        assertContentEquals(data, ms1.message)
 | 
				
			||||||
        assertTrue(pbk in ms1)
 | 
					        assertTrue(pbk in ms1)
 | 
				
			||||||
        assertTrue(p2.publicKey in ms1)
 | 
					        assertTrue(p2.verifying in ms1)
 | 
				
			||||||
        assertTrue(p3.publicKey !in ms1)
 | 
					        assertTrue(p3.verifying !in ms1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        assertThrows<IllegalSignatureException> {
 | 
					        assertThrows<IllegalSignatureException> {
 | 
				
			||||||
            unpack<SealedBox>(pack(ms).also { it[3] = 1u })
 | 
					            unpack<SignedBox>(pack(ms).also { it[3] = 1u })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Test
 | 
				
			||||||
 | 
					    fun secretEncryptTest() = runTest {
 | 
				
			||||||
 | 
					        initCrypto()
 | 
				
			||||||
 | 
					        val key = SecretBox.keygen()
 | 
				
			||||||
 | 
					        val key1 = SecretBox.keygen()
 | 
				
			||||||
 | 
					        assertEquals("hello", decrypt(key, encrypt(key, "hello".encodeToUByteArray())).decodeFromUByteArray())
 | 
				
			||||||
 | 
					        assertThrows<DecryptionFailedException> {
 | 
				
			||||||
 | 
					            decrypt(key, encrypt(key1, "hello".encodeToUByteArray())).decodeFromUByteArray()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,24 +1,11 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import kotlinx.coroutines.test.runTest
 | 
					import kotlinx.coroutines.test.runTest
 | 
				
			||||||
import kotlinx.datetime.Instant
 | 
					import kotlinx.datetime.Instant
 | 
				
			||||||
import net.sergeych.bipack.BipackEncoder
 | 
					import net.sergeych.crypto.initCrypto
 | 
				
			||||||
import net.sergeych.crypto2.initCrypto
 | 
					 | 
				
			||||||
import net.sergeych.kiloparsec.KiloParams
 | 
					 | 
				
			||||||
import net.sergeych.kiloparsec.Transport
 | 
					import net.sergeych.kiloparsec.Transport
 | 
				
			||||||
import net.sergeych.utools.nowToSeconds
 | 
					import net.sergeych.utools.nowToSeconds
 | 
				
			||||||
import net.sergeych.utools.pack
 | 
					import net.sergeych.utools.pack
 | 
				
			||||||
import net.sergeych.utools.unpack
 | 
					import net.sergeych.utools.unpack
 | 
				
			||||||
import kotlin.test.Test
 | 
					import kotlin.test.Test
 | 
				
			||||||
import kotlin.test.assertContentEquals
 | 
					 | 
				
			||||||
import kotlin.test.assertEquals
 | 
					import kotlin.test.assertEquals
 | 
				
			||||||
import kotlin.time.Duration.Companion.microseconds
 | 
					import kotlin.time.Duration.Companion.microseconds
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -53,10 +40,4 @@ class PackTest {
 | 
				
			|||||||
        val b2 = unpack<Transport.Block>(p1)
 | 
					        val b2 = unpack<Transport.Block>(p1)
 | 
				
			||||||
        assertEquals(b1,b2)
 | 
					        assertEquals(b1,b2)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Test
 | 
					 | 
				
			||||||
    fun packPackage() = runTest {
 | 
					 | 
				
			||||||
        val x = BipackEncoder.encode(KiloParams.Package(1u, ubyteArrayOf()))
 | 
					 | 
				
			||||||
        assertContentEquals(byteArrayOf(4, 0), x)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -1,17 +1,20 @@
 | 
				
			|||||||
/*
 | 
					import kotlinx.coroutines.test.runTest
 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					import net.sergeych.crypto.createContrail
 | 
				
			||||||
 *
 | 
					import net.sergeych.crypto.initCrypto
 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					import net.sergeych.crypto.isValidContrail
 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					import kotlin.test.Test
 | 
				
			||||||
 *
 | 
					import kotlin.test.assertEquals
 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					import kotlin.test.assertFalse
 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					import kotlin.test.assertTrue
 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ToolsTest {
 | 
					class ToolsTest {
 | 
				
			||||||
//    @Test
 | 
					    @Test
 | 
				
			||||||
//    fun testRemoceCmd() {
 | 
					    fun testContrails() = runTest {
 | 
				
			||||||
//        assertEquals("lalala", removeCmd("lalala"))
 | 
					        initCrypto()
 | 
				
			||||||
//        assertEquals("lalala", removeCmd("cmdlalala"))
 | 
					        val c = createContrail(ubyteArrayOf(1u, 2u, 3u, 4u, 5u))
 | 
				
			||||||
//    }
 | 
					        assertEquals(134u, c[0])
 | 
				
			||||||
 | 
					        assertTrue { isValidContrail(c) }
 | 
				
			||||||
 | 
					        c[2] = 11u
 | 
				
			||||||
 | 
					        assertFalse { isValidContrail(c) }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -1,27 +1,15 @@
 | 
				
			|||||||
/*
 | 
					import com.ionspin.kotlin.crypto.keyexchange.KeyExchange
 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import kotlinx.coroutines.*
 | 
					import kotlinx.coroutines.*
 | 
				
			||||||
import kotlinx.coroutines.channels.Channel
 | 
					import kotlinx.coroutines.channels.Channel
 | 
				
			||||||
import kotlinx.coroutines.channels.ReceiveChannel
 | 
					import kotlinx.coroutines.channels.ReceiveChannel
 | 
				
			||||||
import kotlinx.coroutines.channels.SendChannel
 | 
					import kotlinx.coroutines.channels.SendChannel
 | 
				
			||||||
import kotlinx.coroutines.test.runTest
 | 
					import kotlinx.coroutines.test.runTest
 | 
				
			||||||
import net.sergeych.crypto2.SigningSecretKey
 | 
					import net.sergeych.crypto.Key
 | 
				
			||||||
import net.sergeych.crypto2.VerifyingPublicKey
 | 
					import net.sergeych.crypto.initCrypto
 | 
				
			||||||
import net.sergeych.crypto2.initCrypto
 | 
					import net.sergeych.kiloparsec.command
 | 
				
			||||||
import net.sergeych.kiloparsec.*
 | 
					import net.sergeych.kiloparsec.*
 | 
				
			||||||
import net.sergeych.mp_logger.Log
 | 
					import net.sergeych.mp_logger.Log
 | 
				
			||||||
import kotlin.test.Test
 | 
					import kotlin.test.*
 | 
				
			||||||
import kotlin.test.assertContentEquals
 | 
					 | 
				
			||||||
import kotlin.test.assertEquals
 | 
					 | 
				
			||||||
import kotlin.test.fail
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
private var dcnt = 0
 | 
					private var dcnt = 0
 | 
				
			||||||
fun createTestDevice(): Pair<Transport.Device, Transport.Device> {
 | 
					fun createTestDevice(): Pair<Transport.Device, Transport.Device> {
 | 
				
			||||||
@ -29,9 +17,8 @@ fun createTestDevice(): Pair<Transport.Device, Transport.Device> {
 | 
				
			|||||||
    val p2 = Channel<UByteArray>(256)
 | 
					    val p2 = Channel<UByteArray>(256)
 | 
				
			||||||
    val id = ++dcnt
 | 
					    val id = ++dcnt
 | 
				
			||||||
    val d1 = object : Transport.Device {
 | 
					    val d1 = object : Transport.Device {
 | 
				
			||||||
        override val input: ReceiveChannel<UByteArray> = p1
 | 
					        override val input: ReceiveChannel<UByteArray?> = p1
 | 
				
			||||||
        override val output: SendChannel<UByteArray> = p2
 | 
					        override val output: SendChannel<UByteArray> = p2
 | 
				
			||||||
 | 
					 | 
				
			||||||
        override suspend fun close() {
 | 
					        override suspend fun close() {
 | 
				
			||||||
            p2.close()
 | 
					            p2.close()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -41,7 +28,7 @@ fun createTestDevice(): Pair<Transport.Device, Transport.Device> {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    val d2 = object : Transport.Device {
 | 
					    val d2 = object : Transport.Device {
 | 
				
			||||||
        override val input: ReceiveChannel<UByteArray> = p2
 | 
					        override val input: ReceiveChannel<UByteArray?> = p2
 | 
				
			||||||
        override val output: SendChannel<UByteArray> = p1
 | 
					        override val output: SendChannel<UByteArray> = p1
 | 
				
			||||||
        override suspend fun close() {
 | 
					        override suspend fun close() {
 | 
				
			||||||
            p1.close()
 | 
					            p1.close()
 | 
				
			||||||
@ -83,29 +70,29 @@ class TransportTest {
 | 
				
			|||||||
        val t1 = Transport(d1, l1, Unit)
 | 
					        val t1 = Transport(d1, l1, Unit)
 | 
				
			||||||
        val t2 = Transport(d2, l2, Unit)
 | 
					        val t2 = Transport(d2, l2, Unit)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//        val clip = KeyExchange.keypair()
 | 
					        val clip = KeyExchange.keypair()
 | 
				
			||||||
//        val serp = KeyExchange.keypair()
 | 
					        val serp = KeyExchange.keypair()
 | 
				
			||||||
//        val clisk = KeyExchange.clientSessionKeys(clip.publicKey, clip.secretKey, serp.publicKey)
 | 
					        val clisk = KeyExchange.clientSessionKeys(clip.publicKey, clip.secretKey, serp.publicKey)
 | 
				
			||||||
//        val sersk = KeyExchange.serverSessionKeys(serp.publicKey, serp.secretKey, clip.publicKey)
 | 
					        val sersk = KeyExchange.serverSessionKeys(serp.publicKey, serp.secretKey, clip.publicKey)
 | 
				
			||||||
//        val pser = KiloParams(true, t1, sersk, Unit, null, t1)
 | 
					        val pser = KiloParams(true, t1, sersk, Unit, null, t1)
 | 
				
			||||||
//        val pcli = KiloParams(false, t2, clisk, Unit, null, t2)
 | 
					        val pcli = KiloParams(false, t2, clisk, Unit, null, t2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assertContentEquals(pcli.token, pser.token)
 | 
				
			||||||
 | 
					        assertEquals(pser.decryptString(pcli.encrypt("hello!")), "hello!")
 | 
				
			||||||
 | 
					        assertEquals(pser.decryptString(pcli.encrypt("hello!")), "hello!")
 | 
				
			||||||
 | 
					        assertEquals(pser.decryptString(pcli.encrypt("hello2!")), "hello2!")
 | 
				
			||||||
 | 
					        assertEquals(pser.decryptString(pcli.encrypt("hello3!")), "hello3!")
 | 
				
			||||||
 | 
					        assertEquals(pser.decryptString(pcli.encrypt("hello!")), "hello!")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//        assertContentEquals(pcli.token, pser.token)
 | 
					 | 
				
			||||||
//        assertEquals(pser.decryptString(pcli.encrypt("hello!")), "hello!")
 | 
					 | 
				
			||||||
//        assertEquals(pser.decryptString(pcli.encrypt("hello!")), "hello!")
 | 
					 | 
				
			||||||
//        assertEquals(pser.decryptString(pcli.encrypt("hello2!")), "hello2!")
 | 
					 | 
				
			||||||
//        assertEquals(pser.decryptString(pcli.encrypt("hello3!")), "hello3!")
 | 
					 | 
				
			||||||
//        assertEquals(pser.decryptString(pcli.encrypt("hello!")), "hello!")
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
        // test nonce increment
 | 
					        // test nonce increment
 | 
				
			||||||
//        assertFalse { pcli.encrypt("once") contentEquals pcli.encrypt("once") }
 | 
					        assertFalse { pcli.encrypt("once") contentEquals pcli.encrypt("once") }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//        assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
 | 
					        assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
 | 
				
			||||||
//        assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
 | 
					        assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
 | 
				
			||||||
//        assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
 | 
					        assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
 | 
				
			||||||
//        assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
 | 
					        assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
 | 
				
			||||||
//        assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
 | 
					        assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
 | 
				
			||||||
//        assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
 | 
					        assertEquals(pcli.decryptString(pser.encrypt("hello!")), "hello!")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        coroutineScope {
 | 
					        coroutineScope {
 | 
				
			||||||
@ -169,8 +156,6 @@ class TransportTest {
 | 
				
			|||||||
        d2.close()
 | 
					        d2.close()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class TestException(text: String) : Exception(text)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Test
 | 
					    @Test
 | 
				
			||||||
    fun testClient() = runTest {
 | 
					    fun testClient() = runTest {
 | 
				
			||||||
        initCrypto()
 | 
					        initCrypto()
 | 
				
			||||||
@ -178,7 +163,7 @@ class TransportTest {
 | 
				
			|||||||
        val cmdPing by command<String, String>()
 | 
					        val cmdPing by command<String, String>()
 | 
				
			||||||
        val cmdPush by command<String, String>()
 | 
					        val cmdPush by command<String, String>()
 | 
				
			||||||
        val cmdGetToken by command<Unit, UByteArray>()
 | 
					        val cmdGetToken by command<Unit, UByteArray>()
 | 
				
			||||||
        val cmdGetClientId by command<Unit, VerifyingPublicKey?>()
 | 
					        val cmdGetClientId by command<Unit, Key.Verifying?>()
 | 
				
			||||||
        val cmdChainCallServer1 by command<String, String>()
 | 
					        val cmdChainCallServer1 by command<String, String>()
 | 
				
			||||||
        val cmdChainCallClient1 by command<String, String>()
 | 
					        val cmdChainCallClient1 by command<String, String>()
 | 
				
			||||||
        val cmdChainCallServer2 by command<String, String>()
 | 
					        val cmdChainCallServer2 by command<String, String>()
 | 
				
			||||||
@ -187,23 +172,13 @@ class TransportTest {
 | 
				
			|||||||
//        Log.defaultLevel = Log.Level.DEBUG
 | 
					//        Log.defaultLevel = Log.Level.DEBUG
 | 
				
			||||||
        val (d1, d2) = createTestDevice()
 | 
					        val (d1, d2) = createTestDevice()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        val serverId = SigningSecretKey.generatePair()
 | 
					        val serverId = Key.Signing.pair()
 | 
				
			||||||
        val clientId = SigningSecretKey.generatePair()
 | 
					        val clientId = Key.Signing.pair()
 | 
				
			||||||
 | 
					 | 
				
			||||||
        val cmdException by command<Unit, Unit>()
 | 
					 | 
				
			||||||
        val cmdRemoteExceptionTest by command<Unit, String>()
 | 
					 | 
				
			||||||
        val cmdBreak by command<Unit, Unit>()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        val cmdPushServer by push<String>()
 | 
					 | 
				
			||||||
        val pushedFromServer = CompletableDeferred<String>()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        val serverInterface = KiloInterface<String>().apply {
 | 
					        val serverInterface = KiloInterface<String>().apply {
 | 
				
			||||||
            on(cmdPing) {
 | 
					            on(cmdPing) {
 | 
				
			||||||
                "pong! [$it]"
 | 
					                "pong! [$it]"
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            on(cmdPushServer) {
 | 
					 | 
				
			||||||
                pushedFromServer.complete(it)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            on(cmdGetToken) {
 | 
					            on(cmdGetToken) {
 | 
				
			||||||
                sessionToken
 | 
					                sessionToken
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -216,28 +191,16 @@ class TransportTest {
 | 
				
			|||||||
            on(cmdChainCallServer2) {
 | 
					            on(cmdChainCallServer2) {
 | 
				
			||||||
                remote.call(cmdChainCallClient2, "$it-s2")
 | 
					                remote.call(cmdChainCallClient2, "$it-s2")
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            on(cmdException) { throw TestException("te1") }
 | 
					            registerError { IllegalStateException() }
 | 
				
			||||||
            on(cmdRemoteExceptionTest) {
 | 
					            registerError { IllegalArgumentException(it) }
 | 
				
			||||||
                try {
 | 
					 | 
				
			||||||
                    remote.call(cmdException)
 | 
					 | 
				
			||||||
                    "error!"
 | 
					 | 
				
			||||||
                } catch (e: TestException) {
 | 
					 | 
				
			||||||
                    "ok: ${e.message}"
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            on(cmdBreak) { throw LocalInterface.BreakConnectionException() }
 | 
					 | 
				
			||||||
            registerError { TestException(it) }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        val kiloServerConnection = KiloServerConnection(
 | 
					        val kiloServerConnection = KiloServerConnection(serverInterface, d1, "server session", serverId.signing)
 | 
				
			||||||
            serverInterface, d1, "server session", serverId.secretKey
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        launch { kiloServerConnection.run() }
 | 
					        launch { kiloServerConnection.run() }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var cnt = 0
 | 
					        var cnt = 0
 | 
				
			||||||
        val client = KiloClient {
 | 
					        val client = KiloClient {
 | 
				
			||||||
            addErrors(serverInterface)
 | 
					 | 
				
			||||||
            session { "client session!" }
 | 
					            session { "client session!" }
 | 
				
			||||||
            secretIdKey = clientId.secretKey
 | 
					            secretIdKey = clientId.signing
 | 
				
			||||||
            local {
 | 
					            local {
 | 
				
			||||||
                on(cmdPush) {
 | 
					                on(cmdPush) {
 | 
				
			||||||
                    "server push: $it"
 | 
					                    "server push: $it"
 | 
				
			||||||
@ -246,13 +209,11 @@ class TransportTest {
 | 
				
			|||||||
                    "client pong: $it"
 | 
					                    "client pong: $it"
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                on(cmdChainCallClient1) {
 | 
					                on(cmdChainCallClient1) {
 | 
				
			||||||
                    remote.call(cmdChainCallServer2, "$it-c1")
 | 
					                    remote.call(cmdChainCallServer2,"$it-c1")
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                on(cmdChainCallClient2) { "$it-c2" }
 | 
					                on(cmdChainCallClient2) { "$it-c2" }
 | 
				
			||||||
                on(cmdException) { throw TestException("te-local") }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            connect {
 | 
					            connect {
 | 
				
			||||||
                println("Called connect: $cnt")
 | 
					 | 
				
			||||||
                if (cnt++ > 0) {
 | 
					                if (cnt++ > 0) {
 | 
				
			||||||
                    cancel()
 | 
					                    cancel()
 | 
				
			||||||
                    fail("connect called once again")
 | 
					                    fail("connect called once again")
 | 
				
			||||||
@ -265,25 +226,7 @@ class TransportTest {
 | 
				
			|||||||
        assertEquals("client pong: foo", kiloServerConnection.call(cmdPing, "foo"))
 | 
					        assertEquals("client pong: foo", kiloServerConnection.call(cmdPing, "foo"))
 | 
				
			||||||
        assertEquals("server push: bar", kiloServerConnection.call(cmdPush, "bar"))
 | 
					        assertEquals("server push: bar", kiloServerConnection.call(cmdPush, "bar"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        client.push(cmdPushServer, "42")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        assertEquals("**-s1-c1-s2-c2", client.call(cmdChainCallServer1, "**"))
 | 
					        assertEquals("**-s1-c1-s2-c2", client.call(cmdChainCallServer1, "**"))
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        assertThrows<TestException> { client.call(cmdException) }
 | 
					 | 
				
			||||||
        assertEquals("ok: te-local", client.call(cmdRemoteExceptionTest))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // wait for push to be received and check
 | 
					 | 
				
			||||||
        assertEquals("42", pushedFromServer.await())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        assertThrows<RemoteInterface.ClosedException> {
 | 
					 | 
				
			||||||
            client.call(cmdBreak)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Note that current transport test is too simple,
 | 
					 | 
				
			||||||
        // therefore, we can't test reconnecting, also we need server and client instances
 | 
					 | 
				
			||||||
        // not connections, so that's all
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        d1.close()
 | 
					        d1.close()
 | 
				
			||||||
        d2.close()
 | 
					        d2.close()
 | 
				
			||||||
        client.close()
 | 
					        client.close()
 | 
				
			||||||
 | 
				
			|||||||
@ -1,13 +1,3 @@
 | 
				
			|||||||
/*
 | 
					 | 
				
			||||||
 * Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 *  You may use, distribute and modify this code under the
 | 
					 | 
				
			||||||
 * terms of the private license, which you must obtain from the author
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * To obtain the license, contact the author: https://t.me/real_sergeych or email to
 | 
					 | 
				
			||||||
 * real dot sergeych at gmail.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import kotlin.test.fail
 | 
					import kotlin.test.fail
 | 
				
			||||||
 | 
					
 | 
				
			||||||
inline fun <reified T: Throwable>assertThrows(f: ()->Unit): T {
 | 
					inline fun <reified T: Throwable>assertThrows(f: ()->Unit): T {
 | 
				
			||||||
@ -18,7 +8,6 @@ inline fun <reified T: Throwable>assertThrows(f: ()->Unit): T {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    catch(x: Throwable) {
 | 
					    catch(x: Throwable) {
 | 
				
			||||||
        if( x is T ) return x
 | 
					        if( x is T ) return x
 | 
				
			||||||
        println("expected to throw $name but instead threw ${x::class.simpleName}: $x\b\n${x.stackTraceToString()}")
 | 
					        fail("expected to throw $name but instead threw ${x::class.simpleName}: $x")
 | 
				
			||||||
        fail("expected to throw $name but instead threw ${x::class.simpleName}: $x\b\n${x.stackTraceToString()}")
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					package net.sergeych.kiloparsec.adapter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					actual fun networkAddressOf(address: String): NetworkAddress {
 | 
				
			||||||
 | 
					    TODO("Not yet implemented")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					package net.sergeych.kiloparsec.adapter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					actual fun NetworkAddress(host: String, port: Int): NetworkAddress {
 | 
				
			||||||
 | 
					    TODO("Not yet implemented")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										6
									
								
								src/jsMain/kotlin/net/sergeych/tools/ProtectedOp.js.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/jsMain/kotlin/net/sergeych/tools/ProtectedOp.js.kt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					package net.sergeych.tools
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					actual fun ProtectedOp(): ProtectedOpImplementation = object : ProtectedOpImplementation {
 | 
				
			||||||
 | 
					    // JS targets are inherently single-threaded, so we do noting:
 | 
				
			||||||
 | 
					    override fun <T> invoke(f: () -> T): T = f()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					package net.sergeych.kiloparsec.adapter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.net.InetAddress
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					actual fun networkAddressOf(address: String): NetworkAddress {
 | 
				
			||||||
 | 
					    val (host,port) = address.split(":")
 | 
				
			||||||
 | 
					    return JvmNetworkAddress(InetAddress.getByName(host), port.toInt())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					package net.sergeych.kiloparsec.adapter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.net.InetAddress
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					actual fun NetworkAddress(host: String, port: Int): NetworkAddress =
 | 
				
			||||||
 | 
					    JvmNetworkAddress(InetAddress.getByName(host), port)
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user