@glassly/bluetooth-sdk 0.1.0-dev.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (548) hide show
  1. package/README.md +563 -0
  2. package/android/build.gradle +450 -0
  3. package/android/gradle.properties.example +9 -0
  4. package/android/lc3Lib/CMakeLists.txt +6 -0
  5. package/android/lc3Lib/build.gradle +212 -0
  6. package/android/lc3Lib/proguard-rules.pro +33 -0
  7. package/android/lc3Lib/src/main/AndroidManifest.xml +2 -0
  8. package/android/lc3Lib/src/main/cpp/CMakeLists.txt +8 -0
  9. package/android/lc3Lib/src/main/cpp/google_opus_stuff/CMakeLists.txt +30 -0
  10. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/CMakeLists.txt +7 -0
  11. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.cc +96 -0
  12. package/android/lc3Lib/src/main/cpp/google_opus_stuff/jni/ogg_opus_encoder.h +53 -0
  13. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/Makefile.am +6 -0
  14. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h +26 -0
  15. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/config_types.h.in +26 -0
  16. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/ogg.h +209 -0
  17. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libogg/os_types.h +158 -0
  18. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus.h +981 -0
  19. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_custom.h +342 -0
  20. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_defines.h +799 -0
  21. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_multistream.h +660 -0
  22. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_projection.h +568 -0
  23. package/android/lc3Lib/src/main/cpp/google_opus_stuff/libopus/opus_types.h +166 -0
  24. package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.cc +268 -0
  25. package/android/lc3Lib/src/main/cpp/google_opus_stuff/ogg_opus_encoder.h +115 -0
  26. package/android/lc3Lib/src/main/cpp/google_opus_stuff/opus_tools/opus_header.h +59 -0
  27. package/android/lc3Lib/src/main/cpp/liblc3/CMakeLists.txt +38 -0
  28. package/android/lc3Lib/src/main/cpp/liblc3/include/lc3.h +309 -0
  29. package/android/lc3Lib/src/main/cpp/liblc3/include/lc3_private.h +162 -0
  30. package/android/lc3Lib/src/main/cpp/liblc3/include/rnnoise.h +114 -0
  31. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/CMakeLists.txt +19 -0
  32. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.c +92 -0
  33. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/attdet.h +44 -0
  34. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.c +375 -0
  35. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bits.h +315 -0
  36. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.c +129 -0
  37. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/bwdet.h +69 -0
  38. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/common.h +148 -0
  39. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.c +70 -0
  40. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/energy.h +43 -0
  41. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/fastmath.h +158 -0
  42. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/lc3.c +702 -0
  43. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.c +893 -0
  44. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf.h +111 -0
  45. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_arm.h +506 -0
  46. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/ltpf_neon.h +281 -0
  47. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/makefile.mk +35 -0
  48. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.c +452 -0
  49. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct.h +57 -0
  50. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/mdct_neon.h +296 -0
  51. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/meson.build +46 -0
  52. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.c +61 -0
  53. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/plc.h +57 -0
  54. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.c +880 -0
  55. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/sns.h +103 -0
  56. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.c +904 -0
  57. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/spec.h +119 -0
  58. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.c +3457 -0
  59. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tables.h +94 -0
  60. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.c +457 -0
  61. package/android/lc3Lib/src/main/cpp/liblc3/liblc3/tns.h +99 -0
  62. package/android/lc3Lib/src/main/cpp/liblc3/liblc3.cpp +159 -0
  63. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/_kiss_fft_guts.h +182 -0
  64. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/arch.h +261 -0
  65. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.c +279 -0
  66. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/celt_lpc.h +59 -0
  67. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/common.h +48 -0
  68. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/compile.sh +3 -0
  69. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/denoise.c +646 -0
  70. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.c +601 -0
  71. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/kiss_fft.h +203 -0
  72. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/opus_types.h +159 -0
  73. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.c +526 -0
  74. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/pitch.h +149 -0
  75. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.c +178 -0
  76. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn.h +69 -0
  77. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.c +11051 -0
  78. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_data.h +34 -0
  79. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_reader.c +168 -0
  80. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/rnn_train.py +66 -0
  81. package/android/lc3Lib/src/main/cpp/liblc3/rnnoise/tansig_table.h +45 -0
  82. package/android/lc3Lib/src/main/java/com/glassly/lc3Lib/Lc3Cpp.java +38 -0
  83. package/android/lc3Lib/third_party/CMakeLists.txt +23 -0
  84. package/android/lc3Lib/third_party/CMakeLists_libogg.txt +18 -0
  85. package/android/lc3Lib/third_party/CMakeLists_libopus.txt +18 -0
  86. package/android/lc3Lib/third_party/CMakeLists_opus-tools.txt +23 -0
  87. package/android/lc3Lib/third_party/opus_tools/src/AUTHORS +5 -0
  88. package/android/lc3Lib/third_party/opus_tools/src/CMakeLists.txt +4 -0
  89. package/android/lc3Lib/third_party/opus_tools/src/COPYING +371 -0
  90. package/android/lc3Lib/third_party/opus_tools/src/ChangeLog +3 -0
  91. package/android/lc3Lib/third_party/opus_tools/src/Makefile.am +42 -0
  92. package/android/lc3Lib/third_party/opus_tools/src/Makefile.unix +23 -0
  93. package/android/lc3Lib/third_party/opus_tools/src/NEWS +0 -0
  94. package/android/lc3Lib/third_party/opus_tools/src/README +0 -0
  95. package/android/lc3Lib/third_party/opus_tools/src/autogen.sh +115 -0
  96. package/android/lc3Lib/third_party/opus_tools/src/configure.ac +293 -0
  97. package/android/lc3Lib/third_party/opus_tools/src/src/arch.h +239 -0
  98. package/android/lc3Lib/third_party/opus_tools/src/src/audio-in.c +1046 -0
  99. package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.c +245 -0
  100. package/android/lc3Lib/third_party/opus_tools/src/src/diag_range.h +28 -0
  101. package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.c +320 -0
  102. package/android/lc3Lib/third_party/opus_tools/src/src/info_opus.h +51 -0
  103. package/android/lc3Lib/third_party/opus_tools/src/src/lpc.c +157 -0
  104. package/android/lc3Lib/third_party/opus_tools/src/src/lpc.h +27 -0
  105. package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.c +286 -0
  106. package/android/lc3Lib/third_party/opus_tools/src/src/opus_header.h +59 -0
  107. package/android/lc3Lib/third_party/opus_tools/src/src/opusdec.c +884 -0
  108. package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.c +1021 -0
  109. package/android/lc3Lib/third_party/opus_tools/src/src/opusenc.h +101 -0
  110. package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.c +639 -0
  111. package/android/lc3Lib/third_party/opus_tools/src/src/opusinfo.h +51 -0
  112. package/android/lc3Lib/third_party/opus_tools/src/src/os_support.h +167 -0
  113. package/android/lc3Lib/third_party/opus_tools/src/src/resample.c +1137 -0
  114. package/android/lc3Lib/third_party/opus_tools/src/src/speex_resampler.h +344 -0
  115. package/android/lc3Lib/third_party/opus_tools/src/src/stack_alloc.h +115 -0
  116. package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.c +125 -0
  117. package/android/lc3Lib/third_party/opus_tools/src/src/wav_io.h +62 -0
  118. package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.c +223 -0
  119. package/android/lc3Lib/third_party/opus_tools/src/src/wave_out.h +60 -0
  120. package/android/settings.gradle +4 -0
  121. package/android/silero/LICENSE.md +17 -0
  122. package/android/silero/build.gradle +50 -0
  123. package/android/silero/proguard-rules.pro +24 -0
  124. package/android/silero/src/androidTest/assets/hello.wav +0 -0
  125. package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
  126. package/android/silero/src/main/assets/LICENSE +21 -0
  127. package/android/silero/src/main/assets/silero_vad.onnx +0 -0
  128. package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
  129. package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
  130. package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
  131. package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
  132. package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
  133. package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
  134. package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
  135. package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
  136. package/android/src/main/AndroidManifest.xml +42 -0
  137. package/android/src/main/assets/G2Firmware/README.md +11 -0
  138. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdk.kt +2325 -0
  139. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkAnalytics.kt +216 -0
  140. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkDebug.kt +14 -0
  141. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkDefaults.kt +7 -0
  142. package/android/src/main/java/com/glassly/bluetoothsdk/BluetoothSdkModule.kt +1088 -0
  143. package/android/src/main/java/com/glassly/bluetoothsdk/Bridge.kt +953 -0
  144. package/android/src/main/java/com/glassly/bluetoothsdk/DeviceManager.kt +2528 -0
  145. package/android/src/main/java/com/glassly/bluetoothsdk/DeviceStore.kt +360 -0
  146. package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedChangelogCatalog.kt +8 -0
  147. package/android/src/main/java/com/glassly/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
  148. package/android/src/main/java/com/glassly/bluetoothsdk/LocalNetworkModule.kt +414 -0
  149. package/android/src/main/java/com/glassly/bluetoothsdk/ObservableStore.kt +88 -0
  150. package/android/src/main/java/com/glassly/bluetoothsdk/OtaManifest.kt +191 -0
  151. package/android/src/main/java/com/glassly/bluetoothsdk/ReleaseChangelog.kt +61 -0
  152. package/android/src/main/java/com/glassly/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
  153. package/android/src/main/java/com/glassly/bluetoothsdk/audio/AudioModels.kt +144 -0
  154. package/android/src/main/java/com/glassly/bluetoothsdk/camera/CameraModels.kt +486 -0
  155. package/android/src/main/java/com/glassly/bluetoothsdk/connection/ConnectionModels.kt +78 -0
  156. package/android/src/main/java/com/glassly/bluetoothsdk/controllers/ControllerManager.kt +158 -0
  157. package/android/src/main/java/com/glassly/bluetoothsdk/controllers/R1.kt +937 -0
  158. package/android/src/main/java/com/glassly/bluetoothsdk/debug/BleTraceLogger.kt +165 -0
  159. package/android/src/main/java/com/glassly/bluetoothsdk/events/BluetoothEvents.kt +178 -0
  160. package/android/src/main/java/com/glassly/bluetoothsdk/internal/MapParsing.kt +140 -0
  161. package/android/src/main/java/com/glassly/bluetoothsdk/net/LocalIpv4.kt +57 -0
  162. package/android/src/main/java/com/glassly/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
  163. package/android/src/main/java/com/glassly/bluetoothsdk/otaserver/OtaServerModule.kt +120 -0
  164. package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
  165. package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +622 -0
  166. package/android/src/main/java/com/glassly/bluetoothsdk/photoreceiver/PhotoReceiverModule.kt +156 -0
  167. package/android/src/main/java/com/glassly/bluetoothsdk/requests/DisplayRequests.kt +56 -0
  168. package/android/src/main/java/com/glassly/bluetoothsdk/services/Foreground.kt +272 -0
  169. package/android/src/main/java/com/glassly/bluetoothsdk/services/PhoneMic.kt +1195 -0
  170. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Ar99.java +2653 -0
  171. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/BesOtaHeartbeatGuard.kt +49 -0
  172. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/BesOtaProgressMapper.kt +27 -0
  173. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G1.kt +3878 -0
  174. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G1TextSanitizer.kt +38 -0
  175. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2.kt +6428 -0
  176. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassification.kt +49 -0
  177. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwRenderer.kt +1241 -0
  178. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCache.kt +270 -0
  179. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocol.kt +291 -0
  180. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/G2Flasher.kt +411 -0
  181. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/GlasslyBle.java +43189 -0
  182. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Mach1.kt +1280 -0
  183. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLive.kt +10817 -0
  184. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLiveL2capChannel.kt +274 -0
  185. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +54 -0
  186. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/MentraNex.kt +2011 -0
  187. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Nimo.kt +2532 -0
  188. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/SGCManager.kt +470 -0
  189. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/Simulated.kt +228 -0
  190. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/Ar99OtaManager.java +566 -0
  191. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaByteUtils.java +18 -0
  192. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaCommandConstants.java +35 -0
  193. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaCrc32Util.java +13 -0
  194. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaGattTransport.java +11 -0
  195. package/android/src/main/java/com/glassly/bluetoothsdk/sgcs/ar99/ota/OtaProtocol.java +92 -0
  196. package/android/src/main/java/com/glassly/bluetoothsdk/speakerid/SpeakerIdEngine.kt +525 -0
  197. package/android/src/main/java/com/glassly/bluetoothsdk/status/DeviceStatus.kt +669 -0
  198. package/android/src/main/java/com/glassly/bluetoothsdk/status/RuntimeState.kt +201 -0
  199. package/android/src/main/java/com/glassly/bluetoothsdk/status/WifiHotspotStatus.kt +189 -0
  200. package/android/src/main/java/com/glassly/bluetoothsdk/streaming/StreamModels.kt +518 -0
  201. package/android/src/main/java/com/glassly/bluetoothsdk/stt/STTTools.kt +391 -0
  202. package/android/src/main/java/com/glassly/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +518 -0
  203. package/android/src/main/java/com/glassly/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
  204. package/android/src/main/java/com/glassly/bluetoothsdk/tts/TTSTools.kt +248 -0
  205. package/android/src/main/java/com/glassly/bluetoothsdk/types/DeviceModels.kt +96 -0
  206. package/android/src/main/java/com/glassly/bluetoothsdk/utils/AES.java +106 -0
  207. package/android/src/main/java/com/glassly/bluetoothsdk/utils/AudioSessionMonitor.kt +189 -0
  208. package/android/src/main/java/com/glassly/bluetoothsdk/utils/AvifExifStripper.java +573 -0
  209. package/android/src/main/java/com/glassly/bluetoothsdk/utils/BitmapJavaUtils.java +169 -0
  210. package/android/src/main/java/com/glassly/bluetoothsdk/utils/BleJsonCompact.java +496 -0
  211. package/android/src/main/java/com/glassly/bluetoothsdk/utils/BlePhotoUploadService.java +677 -0
  212. package/android/src/main/java/com/glassly/bluetoothsdk/utils/BleWireProtocol.java +108 -0
  213. package/android/src/main/java/com/glassly/bluetoothsdk/utils/Constants.kt +46 -0
  214. package/android/src/main/java/com/glassly/bluetoothsdk/utils/G1Text.kt +436 -0
  215. package/android/src/main/java/com/glassly/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
  216. package/android/src/main/java/com/glassly/bluetoothsdk/utils/IncidentLogBleRelayNaming.java +29 -0
  217. package/android/src/main/java/com/glassly/bluetoothsdk/utils/IncidentLogBleUploadService.java +77 -0
  218. package/android/src/main/java/com/glassly/bluetoothsdk/utils/K900LengthCodec.java +115 -0
  219. package/android/src/main/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtils.java +827 -0
  220. package/android/src/main/java/com/glassly/bluetoothsdk/utils/MessageChunkReassembler.java +228 -0
  221. package/android/src/main/java/com/glassly/bluetoothsdk/utils/MessageChunker.java +325 -0
  222. package/android/src/main/java/com/glassly/bluetoothsdk/utils/NexSGCUtils.kt +698 -0
  223. package/android/src/main/java/com/glassly/bluetoothsdk/utils/PhoneAudioMonitor.kt +348 -0
  224. package/android/src/main/java/com/glassly/bluetoothsdk/utils/SmartGlassesConnectionState.java +9 -0
  225. package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/Ar99OpusPcmDecoder.java +398 -0
  226. package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/ByteUtilAudioPlayer.java +655 -0
  227. package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/Lc3Player.java +441 -0
  228. package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/PCMAudioPlayer.java +431 -0
  229. package/android/src/main/java/com/glassly/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +328 -0
  230. package/android/src/main/res/values/strings.xml +4 -0
  231. package/android/src/test/java/com/glassly/bluetoothsdk/BluetoothSdkExceptionTest.kt +18 -0
  232. package/android/src/test/java/com/glassly/bluetoothsdk/OtaManifestDowngradeTest.kt +86 -0
  233. package/android/src/test/java/com/glassly/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
  234. package/android/src/test/java/com/glassly/bluetoothsdk/ReleaseChangelogTest.kt +13 -0
  235. package/android/src/test/java/com/glassly/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
  236. package/android/src/test/java/com/glassly/bluetoothsdk/StreamStatusEventTest.kt +29 -0
  237. package/android/src/test/java/com/glassly/bluetoothsdk/camera/PhotoRequestTest.kt +141 -0
  238. package/android/src/test/java/com/glassly/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
  239. package/android/src/test/java/com/glassly/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
  240. package/android/src/test/java/com/glassly/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
  241. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/Ar99AdvertisementMatcherTest.java +22 -0
  242. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/BesOtaHeartbeatGuardTest.kt +73 -0
  243. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/BesOtaProgressMapperTest.kt +67 -0
  244. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/EvenRleTest.kt +63 -0
  245. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +31 -0
  246. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwClassificationTest.kt +89 -0
  247. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwRendererTest.kt +344 -0
  248. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2CfwTextureCacheTest.kt +200 -0
  249. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/G2FlashProtocolTest.kt +131 -0
  250. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
  251. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +133 -0
  252. package/android/src/test/java/com/glassly/bluetoothsdk/sgcs/NimoProtocolTest.kt +333 -0
  253. package/android/src/test/java/com/glassly/bluetoothsdk/utils/AvifExifStripperTest.java +46 -0
  254. package/android/src/test/java/com/glassly/bluetoothsdk/utils/BinaryMessageChunkerTest.java +84 -0
  255. package/android/src/test/java/com/glassly/bluetoothsdk/utils/BleJsonCompactTest.java +319 -0
  256. package/android/src/test/java/com/glassly/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +107 -0
  257. package/android/src/test/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtilsEndiannessTest.java +172 -0
  258. package/android/src/test/java/com/glassly/bluetoothsdk/utils/K900ProtocolUtilsTest.java +24 -0
  259. package/android/src/test/java/com/glassly/bluetoothsdk/utils/PhoneAudioMonitorTest.kt +45 -0
  260. package/android/src/test/resources/avif_with_exif.avif +0 -0
  261. package/app.plugin.js +3 -0
  262. package/build/BluetoothSdk.types.d.ts +1354 -0
  263. package/build/BluetoothSdk.types.d.ts.map +1 -0
  264. package/build/BluetoothSdk.types.js +41 -0
  265. package/build/BluetoothSdk.types.js.map +1 -0
  266. package/build/_internal.d.ts +17 -0
  267. package/build/_internal.d.ts.map +1 -0
  268. package/build/_internal.js +16 -0
  269. package/build/_internal.js.map +1 -0
  270. package/build/_private/BluetoothSdkModule.d.ts +172 -0
  271. package/build/_private/BluetoothSdkModule.d.ts.map +1 -0
  272. package/build/_private/BluetoothSdkModule.js +300 -0
  273. package/build/_private/BluetoothSdkModule.js.map +1 -0
  274. package/build/_private/LocalNetworkModule.d.ts +38 -0
  275. package/build/_private/LocalNetworkModule.d.ts.map +1 -0
  276. package/build/_private/LocalNetworkModule.js +3 -0
  277. package/build/_private/LocalNetworkModule.js.map +1 -0
  278. package/build/_private/cameraRequestPayload.d.ts +4 -0
  279. package/build/_private/cameraRequestPayload.d.ts.map +1 -0
  280. package/build/_private/cameraRequestPayload.js +32 -0
  281. package/build/_private/cameraRequestPayload.js.map +1 -0
  282. package/build/_private/photoRequestPayload.d.ts +6 -0
  283. package/build/_private/photoRequestPayload.d.ts.map +1 -0
  284. package/build/_private/photoRequestPayload.js +86 -0
  285. package/build/_private/photoRequestPayload.js.map +1 -0
  286. package/build/_private/sdkOtaManifest.d.ts +11 -0
  287. package/build/_private/sdkOtaManifest.d.ts.map +1 -0
  288. package/build/_private/sdkOtaManifest.js +15 -0
  289. package/build/_private/sdkOtaManifest.js.map +1 -0
  290. package/build/changelogs.d.ts +7 -0
  291. package/build/changelogs.d.ts.map +1 -0
  292. package/build/changelogs.js +46 -0
  293. package/build/changelogs.js.map +1 -0
  294. package/build/debug.d.ts +3 -0
  295. package/build/debug.d.ts.map +1 -0
  296. package/build/debug.js +8 -0
  297. package/build/debug.js.map +1 -0
  298. package/build/generated/changelogCatalog.d.ts +9 -0
  299. package/build/generated/changelogCatalog.d.ts.map +1 -0
  300. package/build/generated/changelogCatalog.js +12 -0
  301. package/build/generated/changelogCatalog.js.map +1 -0
  302. package/build/generated/releaseMetadata.d.ts +12 -0
  303. package/build/generated/releaseMetadata.d.ts.map +1 -0
  304. package/build/generated/releaseMetadata.js +10 -0
  305. package/build/generated/releaseMetadata.js.map +1 -0
  306. package/build/index.d.ts +6 -0
  307. package/build/index.d.ts.map +1 -0
  308. package/build/index.js +171 -0
  309. package/build/index.js.map +1 -0
  310. package/build/ota-server/OtaServer.types.d.ts +14 -0
  311. package/build/ota-server/OtaServer.types.d.ts.map +1 -0
  312. package/build/ota-server/OtaServer.types.js +2 -0
  313. package/build/ota-server/OtaServer.types.js.map +1 -0
  314. package/build/ota-server/OtaServerModule.d.ts +21 -0
  315. package/build/ota-server/OtaServerModule.d.ts.map +1 -0
  316. package/build/ota-server/OtaServerModule.js +3 -0
  317. package/build/ota-server/OtaServerModule.js.map +1 -0
  318. package/build/ota-server/OtaServerModule.web.d.ts +10 -0
  319. package/build/ota-server/OtaServerModule.web.d.ts.map +1 -0
  320. package/build/ota-server/OtaServerModule.web.js +12 -0
  321. package/build/ota-server/OtaServerModule.web.js.map +1 -0
  322. package/build/ota-server/index.d.ts +3 -0
  323. package/build/ota-server/index.d.ts.map +1 -0
  324. package/build/ota-server/index.js +3 -0
  325. package/build/ota-server/index.js.map +1 -0
  326. package/build/ota-transport/index.d.ts +67 -0
  327. package/build/ota-transport/index.d.ts.map +1 -0
  328. package/build/ota-transport/index.js +58 -0
  329. package/build/ota-transport/index.js.map +1 -0
  330. package/build/photo-receiver/PhotoReceiver.types.d.ts +18 -0
  331. package/build/photo-receiver/PhotoReceiver.types.d.ts.map +1 -0
  332. package/build/photo-receiver/PhotoReceiver.types.js +2 -0
  333. package/build/photo-receiver/PhotoReceiver.types.js.map +1 -0
  334. package/build/photo-receiver/PhotoReceiverModule.d.ts +10 -0
  335. package/build/photo-receiver/PhotoReceiverModule.d.ts.map +1 -0
  336. package/build/photo-receiver/PhotoReceiverModule.js +3 -0
  337. package/build/photo-receiver/PhotoReceiverModule.js.map +1 -0
  338. package/build/photo-receiver/PhotoReceiverModule.web.d.ts +10 -0
  339. package/build/photo-receiver/PhotoReceiverModule.web.d.ts.map +1 -0
  340. package/build/photo-receiver/PhotoReceiverModule.web.js +12 -0
  341. package/build/photo-receiver/PhotoReceiverModule.web.js.map +1 -0
  342. package/build/photo-receiver/index.d.ts +3 -0
  343. package/build/photo-receiver/index.d.ts.map +1 -0
  344. package/build/photo-receiver/index.js +3 -0
  345. package/build/photo-receiver/index.js.map +1 -0
  346. package/build/react/index.d.ts +5 -0
  347. package/build/react/index.d.ts.map +1 -0
  348. package/build/react/index.js +4 -0
  349. package/build/react/index.js.map +1 -0
  350. package/build/react/useBluetooth.d.ts +100 -0
  351. package/build/react/useBluetooth.d.ts.map +1 -0
  352. package/build/react/useBluetooth.js +167 -0
  353. package/build/react/useBluetooth.js.map +1 -0
  354. package/build/react/useBluetoothEvent.d.ts +6 -0
  355. package/build/react/useBluetoothEvent.d.ts.map +1 -0
  356. package/build/react/useBluetoothEvent.js +21 -0
  357. package/build/react/useBluetoothEvent.js.map +1 -0
  358. package/build/react/useBluetoothScan.d.ts +22 -0
  359. package/build/react/useBluetoothScan.d.ts.map +1 -0
  360. package/build/react/useBluetoothScan.js +135 -0
  361. package/build/react/useBluetoothScan.js.map +1 -0
  362. package/build/react/useBluetoothStatus.d.ts +16 -0
  363. package/build/react/useBluetoothStatus.d.ts.map +1 -0
  364. package/build/react/useBluetoothStatus.js +137 -0
  365. package/build/react/useBluetoothStatus.js.map +1 -0
  366. package/build/react/useGlassesConnection.d.ts +29 -0
  367. package/build/react/useGlassesConnection.d.ts.map +1 -0
  368. package/build/react/useGlassesConnection.js +153 -0
  369. package/build/react/useGlassesConnection.js.map +1 -0
  370. package/build/types/index.d.ts +3 -0
  371. package/build/types/index.d.ts.map +1 -0
  372. package/build/types/index.js +2 -0
  373. package/build/types/index.js.map +1 -0
  374. package/expo-module.config.json +14 -0
  375. package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
  376. package/ios/BluetoothSdkModule.swift +1034 -0
  377. package/ios/GlasslyBluetoothSDK.podspec +106 -0
  378. package/ios/LocalIPv4.swift +150 -0
  379. package/ios/LocalOtaServer.swift +309 -0
  380. package/ios/LocalPhotoUploadServer.swift +754 -0
  381. package/ios/OtaServerModule.swift +147 -0
  382. package/ios/Packages/CoreObjC/CoreObjC.xcodeproj/project.pbxproj +213 -0
  383. package/ios/Packages/CoreObjC/PcmConverter.h +22 -0
  384. package/ios/Packages/CoreObjC/PcmConverter.m +266 -0
  385. package/ios/Packages/CoreObjC/attdet.c +92 -0
  386. package/ios/Packages/CoreObjC/attdet.h +44 -0
  387. package/ios/Packages/CoreObjC/bits.c +375 -0
  388. package/ios/Packages/CoreObjC/bits.h +315 -0
  389. package/ios/Packages/CoreObjC/bwdet.c +129 -0
  390. package/ios/Packages/CoreObjC/bwdet.h +69 -0
  391. package/ios/Packages/CoreObjC/common.h +151 -0
  392. package/ios/Packages/CoreObjC/energy.c +70 -0
  393. package/ios/Packages/CoreObjC/energy.h +43 -0
  394. package/ios/Packages/CoreObjC/fastmath.h +158 -0
  395. package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
  396. package/ios/Packages/CoreObjC/lc3.c +704 -0
  397. package/ios/Packages/CoreObjC/lc3.h +313 -0
  398. package/ios/Packages/CoreObjC/lc3_cpp.h +283 -0
  399. package/ios/Packages/CoreObjC/lc3_private.h +163 -0
  400. package/ios/Packages/CoreObjC/ltpf.c +905 -0
  401. package/ios/Packages/CoreObjC/ltpf.h +111 -0
  402. package/ios/Packages/CoreObjC/ltpf_arm.h +506 -0
  403. package/ios/Packages/CoreObjC/ltpf_neon.h +281 -0
  404. package/ios/Packages/CoreObjC/makefile.mk +35 -0
  405. package/ios/Packages/CoreObjC/mdct.c +469 -0
  406. package/ios/Packages/CoreObjC/mdct.h +57 -0
  407. package/ios/Packages/CoreObjC/mdct_neon.h +296 -0
  408. package/ios/Packages/CoreObjC/meson.build +61 -0
  409. package/ios/Packages/CoreObjC/plc.c +61 -0
  410. package/ios/Packages/CoreObjC/plc.h +57 -0
  411. package/ios/Packages/CoreObjC/rnnoise.h +114 -0
  412. package/ios/Packages/CoreObjC/sns.c +880 -0
  413. package/ios/Packages/CoreObjC/sns.h +103 -0
  414. package/ios/Packages/CoreObjC/spec.c +907 -0
  415. package/ios/Packages/CoreObjC/spec.h +119 -0
  416. package/ios/Packages/CoreObjC/tables.c +3457 -0
  417. package/ios/Packages/CoreObjC/tables.h +94 -0
  418. package/ios/Packages/CoreObjC/tns.c +457 -0
  419. package/ios/Packages/CoreObjC/tns.h +99 -0
  420. package/ios/Packages/SherpaOnnx/Model/joiner.onnx +0 -0
  421. package/ios/Packages/SherpaOnnx/Model/tokens.txt +502 -0
  422. package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +2383 -0
  423. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +48 -0
  424. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4689 -0
  425. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +2036 -0
  426. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
  427. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4689 -0
  428. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +2036 -0
  429. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
  430. package/ios/Packages/libbz2/module.modulemap +5 -0
  431. package/ios/Packages/libbz2/shim.h +1 -0
  432. package/ios/PhotoReceiverModule.swift +129 -0
  433. package/ios/Source/BluetoothSDK.swift +2303 -0
  434. package/ios/Source/BluetoothSDKDebug.swift +12 -0
  435. package/ios/Source/BluetoothSdkDefaults.swift +29 -0
  436. package/ios/Source/Bridge.swift +708 -0
  437. package/ios/Source/Bridging-Header.h +16 -0
  438. package/ios/Source/DeviceManager.swift +2109 -0
  439. package/ios/Source/DeviceStore.swift +336 -0
  440. package/ios/Source/GeneratedChangelogCatalog.swift +7 -0
  441. package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
  442. package/ios/Source/ObservableStore.swift +118 -0
  443. package/ios/Source/OtaManifest.swift +211 -0
  444. package/ios/Source/PrivacyInfo.xcprivacy +23 -0
  445. package/ios/Source/ReleaseChangelog.swift +65 -0
  446. package/ios/Source/Resources/G2Firmware/README.md +10 -0
  447. package/ios/Source/audio/AudioModels.swift +178 -0
  448. package/ios/Source/audio/PcmStreamPlayer.swift +410 -0
  449. package/ios/Source/camera/CameraModels.swift +866 -0
  450. package/ios/Source/connection/ScanSession.swift +27 -0
  451. package/ios/Source/controllers/ControllerManager.swift +184 -0
  452. package/ios/Source/controllers/R1.swift +829 -0
  453. package/ios/Source/errors/BluetoothSdkError.swift +19 -0
  454. package/ios/Source/events/BluetoothEvents.swift +342 -0
  455. package/ios/Source/internal/BleTraceLogger.swift +192 -0
  456. package/ios/Source/internal/BluetoothAvailability.swift +78 -0
  457. package/ios/Source/internal/BluetoothSdkAnalytics.swift +199 -0
  458. package/ios/Source/internal/ValueParsing.swift +99 -0
  459. package/ios/Source/requests/DisplayRequests.swift +82 -0
  460. package/ios/Source/services/PhoneMic.swift +662 -0
  461. package/ios/Source/sgcs/Ar99.swift +2451 -0
  462. package/ios/Source/sgcs/Frame.swift +649 -0
  463. package/ios/Source/sgcs/G1.swift +2554 -0
  464. package/ios/Source/sgcs/G2.swift +6887 -0
  465. package/ios/Source/sgcs/G2CfwClassification.swift +43 -0
  466. package/ios/Source/sgcs/G2CfwRenderer.swift +1279 -0
  467. package/ios/Source/sgcs/G2CfwTextureCache.swift +233 -0
  468. package/ios/Source/sgcs/G2FlashProtocol.swift +312 -0
  469. package/ios/Source/sgcs/G2Flasher.swift +773 -0
  470. package/ios/Source/sgcs/Mach1.swift +541 -0
  471. package/ios/Source/sgcs/MentraLive.swift +7011 -0
  472. package/ios/Source/sgcs/MentraLiveL2capChannel.swift +183 -0
  473. package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +64 -0
  474. package/ios/Source/sgcs/MentraNex.swift +3221 -0
  475. package/ios/Source/sgcs/Nimo.swift +1886 -0
  476. package/ios/Source/sgcs/SGCManager.swift +608 -0
  477. package/ios/Source/sgcs/Simulated.swift +274 -0
  478. package/ios/Source/sgcs/ar99/ota/Ar99OtaManager.swift +637 -0
  479. package/ios/Source/sgcs/glassly_ble.pb.swift +4354 -0
  480. package/ios/Source/speakerid/SpeakerIdEngine.swift +573 -0
  481. package/ios/Source/status/DeviceStatus.swift +981 -0
  482. package/ios/Source/status/RuntimeState.swift +359 -0
  483. package/ios/Source/status/WifiHotspotStatus.swift +345 -0
  484. package/ios/Source/streaming/StreamModels.swift +693 -0
  485. package/ios/Source/stt/STTTools.swift +191 -0
  486. package/ios/Source/stt/SherpaOnnxTranscriber.swift +466 -0
  487. package/ios/Source/tts/TTSTools.swift +171 -0
  488. package/ios/Source/types/DeviceModels.swift +177 -0
  489. package/ios/Source/utils/AudioSessionMonitor.swift +283 -0
  490. package/ios/Source/utils/BinaryCodec.swift +61 -0
  491. package/ios/Source/utils/BleJsonCompact.swift +405 -0
  492. package/ios/Source/utils/BleWireProtocol.swift +92 -0
  493. package/ios/Source/utils/Constants.swift +78 -0
  494. package/ios/Source/utils/Enums.swift +95 -0
  495. package/ios/Source/utils/G1Text.swift +2096 -0
  496. package/ios/Source/utils/JSCExperiment.swift +241 -0
  497. package/ios/Source/utils/MemoryMonitor.swift +44 -0
  498. package/ios/Source/utils/MessageChunkReassembler.swift +190 -0
  499. package/ios/Source/utils/MessageChunker.swift +294 -0
  500. package/ios/Source/utils/Models.swift +135 -0
  501. package/ios/Source/utils/PhoneAudioMonitor.swift +230 -0
  502. package/ios/Source/utils/TarBz2Extractor.swift +257 -0
  503. package/ios/Tests/BluetoothSDKTests/G2CfwClassificationTests.swift +78 -0
  504. package/ios/Tests/BluetoothSDKTests/G2CfwRendererTests.swift +372 -0
  505. package/ios/Tests/BluetoothSDKTests/G2CfwTextureCacheTests.swift +162 -0
  506. package/ios/Tests/BluetoothSDKTests/G2FlashProtocolTests.swift +182 -0
  507. package/ios/Tests/MentraLivePairingAdvertisementTests.swift +160 -0
  508. package/ios/Tests/ReleaseChangelogTests.swift +13 -0
  509. package/package.json +129 -0
  510. package/plugin/build/index.d.ts +10 -0
  511. package/plugin/build/index.js +12 -0
  512. package/plugin/build/withAndroid.d.ts +3 -0
  513. package/plugin/build/withAndroid.js +190 -0
  514. package/plugin/build/withIos.d.ts +5 -0
  515. package/plugin/build/withIos.js +108 -0
  516. package/scripts/inject-ios-sdk-version.mjs +65 -0
  517. package/scripts/public-ota-api.test.mjs +37 -0
  518. package/scripts/verify-release-package.mjs +45 -0
  519. package/scripts/write-release-metadata.mjs +152 -0
  520. package/scripts/write-release-metadata.test.mjs +37 -0
  521. package/src/BluetoothSdk.types.ts +1623 -0
  522. package/src/_internal.ts +16 -0
  523. package/src/_private/BluetoothSdkModule.ts +684 -0
  524. package/src/_private/LocalNetworkModule.ts +50 -0
  525. package/src/_private/cameraRequestPayload.ts +34 -0
  526. package/src/_private/photoRequestPayload.ts +90 -0
  527. package/src/_private/sdkOtaManifest.ts +18 -0
  528. package/src/changelogs.ts +46 -0
  529. package/src/debug.ts +9 -0
  530. package/src/generated/changelogCatalog.ts +13 -0
  531. package/src/generated/releaseMetadata.ts +20 -0
  532. package/src/index.ts +321 -0
  533. package/src/ota-server/OtaServer.types.ts +10 -0
  534. package/src/ota-server/OtaServerModule.ts +23 -0
  535. package/src/ota-server/OtaServerModule.web.ts +17 -0
  536. package/src/ota-server/index.ts +2 -0
  537. package/src/ota-transport/index.ts +116 -0
  538. package/src/photo-receiver/PhotoReceiver.types.ts +20 -0
  539. package/src/photo-receiver/PhotoReceiverModule.ts +11 -0
  540. package/src/photo-receiver/PhotoReceiverModule.web.ts +17 -0
  541. package/src/photo-receiver/index.ts +2 -0
  542. package/src/react/index.ts +21 -0
  543. package/src/react/useBluetooth.ts +309 -0
  544. package/src/react/useBluetoothEvent.ts +35 -0
  545. package/src/react/useBluetoothScan.ts +173 -0
  546. package/src/react/useBluetoothStatus.ts +180 -0
  547. package/src/react/useGlassesConnection.ts +202 -0
  548. package/src/types/index.ts +12 -0
@@ -0,0 +1,754 @@
1
+ import Foundation
2
+ import Network
3
+
4
+ struct PhotoUpload {
5
+ let requestId: String?
6
+ let photoFile: URL
7
+ let byteCount: Int
8
+ let fields: [String: String]
9
+ }
10
+
11
+ final class LocalPhotoUploadServer {
12
+ private let queue = DispatchQueue(label: "com.glassly.examples.photo-upload")
13
+ private let uploadDirectory: URL
14
+ private let onLog: (String) -> Void
15
+ private let onUpload: (PhotoUpload) -> Void
16
+ private var listener: NWListener?
17
+ private var listenerGeneration = 0
18
+ private(set) var activePort: UInt16?
19
+
20
+ init(onLog: @escaping (String) -> Void, onUpload: @escaping (PhotoUpload) -> Void) {
21
+ self.onLog = onLog
22
+ self.onUpload = onUpload
23
+ uploadDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
24
+ .appendingPathComponent("glassly-photo-uploads", isDirectory: true)
25
+ }
26
+
27
+ func start(port: UInt16) throws -> UInt16 {
28
+ if listener != nil, activePort == port {
29
+ onLog("Already listening on 0.0.0.0:\(port)")
30
+ return port
31
+ }
32
+ stop()
33
+ try FileManager.default.createDirectory(
34
+ at: uploadDirectory,
35
+ withIntermediateDirectories: true
36
+ )
37
+
38
+ let parameters = NWParameters.tcp
39
+ parameters.allowLocalEndpointReuse = true
40
+ guard let endpointPort = NWEndpoint.Port(rawValue: port) else {
41
+ throw PhotoUploadServerError("Invalid port \(port)")
42
+ }
43
+
44
+ let listener = try NWListener(using: parameters, on: endpointPort)
45
+ listenerGeneration += 1
46
+ let generation = listenerGeneration
47
+ let started = DispatchSemaphore(value: 0)
48
+ var startError: Error?
49
+ var isReady = false
50
+
51
+ listener.stateUpdateHandler = { [weak self] state in
52
+ guard let self, self.listenerGeneration == generation else {
53
+ return
54
+ }
55
+ switch state {
56
+ case .ready:
57
+ isReady = true
58
+ self.activePort = port
59
+ self.onLog("Listening on 0.0.0.0:\(port)")
60
+ started.signal()
61
+ case .failed(let error):
62
+ startError = error
63
+ self.activePort = nil
64
+ self.listener = nil
65
+ self.onLog("Photo receiver failed on \(port): \(error)")
66
+ started.signal()
67
+ case .cancelled:
68
+ self.activePort = nil
69
+ if !isReady {
70
+ startError = PhotoUploadServerError("Listener cancelled")
71
+ started.signal()
72
+ }
73
+ default:
74
+ break
75
+ }
76
+ }
77
+
78
+ listener.newConnectionHandler = { [weak self] connection in
79
+ self?.handle(connection)
80
+ }
81
+
82
+ self.listener = listener
83
+ listener.start(queue: queue)
84
+
85
+ if started.wait(timeout: .now() + 2) == .timedOut {
86
+ self.listener = nil
87
+ listener.cancel()
88
+ throw PhotoUploadServerError("Timed out starting listener")
89
+ }
90
+ if let startError {
91
+ self.listener = nil
92
+ listener.cancel()
93
+ throw startError
94
+ }
95
+
96
+ return port
97
+ }
98
+
99
+ func stop() {
100
+ listenerGeneration += 1
101
+ listener?.cancel()
102
+ listener = nil
103
+ activePort = nil
104
+ }
105
+
106
+ private func handle(_ connection: NWConnection) {
107
+ traceWifiInput("photo_receiver_connection", connection: connection, state: nil)
108
+ connection.start(queue: queue)
109
+ receive(connection, state: RequestReadState())
110
+ }
111
+
112
+ private func receive(_ connection: NWConnection, state: RequestReadState) {
113
+ connection.receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) {
114
+ [weak self] data, _, isComplete, error in
115
+ guard let self else {
116
+ connection.cancel()
117
+ return
118
+ }
119
+ if let error {
120
+ state.multipartParser?.cleanup()
121
+ self.onLog("request failed: \(error.localizedDescription)")
122
+ connection.cancel()
123
+ return
124
+ }
125
+ if let data, !data.isEmpty {
126
+ state.buffer.append(data)
127
+ }
128
+
129
+ do {
130
+ if try self.handleIfComplete(connection, state: state) {
131
+ return
132
+ }
133
+ } catch {
134
+ state.multipartParser?.cleanup()
135
+ self.onLog("request failed: \(error.localizedDescription)")
136
+ if let uploadError = error as? PhotoUploadServerError {
137
+ self.writeJson(
138
+ connection,
139
+ status: 400,
140
+ body: #"{"ok":false,"error":\#(self.jsonString(uploadError.localizedDescription))}"#
141
+ )
142
+ } else {
143
+ self.writeJson(connection, status: 500, body: #"{"ok":false,"error":"server_error"}"#)
144
+ }
145
+ return
146
+ }
147
+
148
+ if isComplete {
149
+ state.multipartParser?.cleanup()
150
+ self.writeJson(connection, status: 400, body: #"{"ok":false,"error":"incomplete_request"}"#)
151
+ return
152
+ }
153
+ self.receive(connection, state: state)
154
+ }
155
+ }
156
+
157
+ private func handleIfComplete(_ connection: NWConnection, state: RequestReadState) throws -> Bool {
158
+ if state.request == nil {
159
+ guard let headerRange = state.buffer.range(of: Data([13, 10, 13, 10])) else {
160
+ if state.buffer.count > Self.maxHeaderBytes {
161
+ writeJson(connection, status: 413, body: #"{"ok":false,"error":"headers_too_large"}"#)
162
+ return true
163
+ }
164
+ return false
165
+ }
166
+
167
+ let headerData = state.buffer[state.buffer.startIndex..<headerRange.lowerBound]
168
+ guard let headerText = String(data: headerData, encoding: .isoLatin1) else {
169
+ writeJson(connection, status: 400, body: #"{"ok":false,"error":"invalid_headers"}"#)
170
+ return true
171
+ }
172
+ let request = HttpRequest.parse(headerText)
173
+ state.request = request
174
+ onLog("\(request.method) \(request.path)")
175
+ var requestTraceValues: [String: Any] = [
176
+ "method": request.method,
177
+ "path": request.path,
178
+ ]
179
+ if let contentLength = request.headers["content-length"].flatMap(Int.init) {
180
+ requestTraceValues["contentLength"] = contentLength
181
+ }
182
+ traceWifiInput("photo_receiver_request", connection: connection, state: state, values: requestTraceValues)
183
+
184
+ if request.method == "GET", request.path == "/" || request.path == "/health" {
185
+ writeJson(connection, status: 200, body: #"{"ok":true,"service":"glassly-photo-upload-receiver"}"#)
186
+ return true
187
+ }
188
+
189
+ guard request.method == "POST", request.path == "/upload" else {
190
+ writeJson(connection, status: 404, body: #"{"ok":false,"error":"not_found"}"#)
191
+ return true
192
+ }
193
+
194
+ guard let contentLength = request.headers["content-length"].flatMap(Int.init), contentLength > 0 else {
195
+ writeJson(connection, status: 411, body: #"{"ok":false,"error":"content_length_required"}"#)
196
+ return true
197
+ }
198
+ guard contentLength <= Self.maxUploadBytes else {
199
+ writeJson(connection, status: 413, body: #"{"ok":false,"error":"upload_too_large"}"#)
200
+ return true
201
+ }
202
+
203
+ state.contentLength = contentLength
204
+ guard let boundary = multipartBoundary(request.headers["content-type"] ?? "") else {
205
+ writeJson(connection, status: 400, body: #"{"ok":false,"error":"multipart_boundary_required"}"#)
206
+ return true
207
+ }
208
+ state.multipartParser = MultipartStreamParser(
209
+ boundary: boundary,
210
+ uploadDirectory: uploadDirectory
211
+ )
212
+ if request.headers["expect"]?.localizedCaseInsensitiveContains("100-continue") == true {
213
+ write(connection, text: "HTTP/1.1 100 Continue\r\n\r\n", closeAfterSend: false)
214
+ }
215
+
216
+ let bodyFragment = Data(state.buffer[headerRange.upperBound..<state.buffer.endIndex])
217
+ state.buffer.removeAll(keepingCapacity: true)
218
+ try appendBodyFragment(bodyFragment, to: state)
219
+ }
220
+
221
+ guard let request = state.request else {
222
+ return false
223
+ }
224
+ if !state.buffer.isEmpty {
225
+ let bodyFragment = state.buffer
226
+ state.buffer.removeAll(keepingCapacity: true)
227
+ try appendBodyFragment(bodyFragment, to: state)
228
+ }
229
+
230
+ guard state.multipartParser?.isComplete == true else {
231
+ if state.bodyBytesReceived >= state.contentLength {
232
+ state.multipartParser?.cleanup()
233
+ writeJson(connection, status: 400, body: #"{"ok":false,"error":"multipart_boundary_missing"}"#)
234
+ return true
235
+ }
236
+ return false
237
+ }
238
+
239
+ let contentType = request.headers["content-type"] ?? ""
240
+ onLog("headers contentType=\(contentType) contentLength=\(state.contentLength)")
241
+
242
+ guard let parsed = state.multipartParser?.parsedMultipart() else {
243
+ state.multipartParser?.cleanup()
244
+ writeJson(connection, status: 400, body: #"{"ok":false,"error":"incomplete_request"}"#)
245
+ return true
246
+ }
247
+ guard let photoPart = parsed.files["photo"] ?? parsed.files.values.first else {
248
+ parsed.deleteFiles()
249
+ writeJson(connection, status: 400, body: #"{"ok":false,"error":"photo_field_required"}"#)
250
+ return true
251
+ }
252
+
253
+ let requestId = parsed.fields["requestId"] ?? parsed.fields["request_id"]
254
+ let filename = safeFilePart(requestId ?? "photo-\(Int(Date().timeIntervalSince1970 * 1000))")
255
+ let photoFile = uploadDirectory.appendingPathComponent("\(filename).jpg")
256
+ var didMovePhoto = false
257
+ defer {
258
+ if didMovePhoto {
259
+ parsed.deleteFiles(except: photoFile)
260
+ } else {
261
+ parsed.deleteFiles()
262
+ try? FileManager.default.removeItem(at: photoFile)
263
+ }
264
+ }
265
+ try moveFile(photoPart.file, to: photoFile)
266
+ didMovePhoto = true
267
+ let upload = PhotoUpload(
268
+ requestId: requestId,
269
+ photoFile: photoFile,
270
+ byteCount: photoPart.byteCount,
271
+ fields: parsed.fields
272
+ )
273
+
274
+ onLog("upload fields=\(parsed.fields.keys.joined(separator: ",")) requestId=\(requestId ?? "") bytes=\(photoPart.byteCount) saved=\(photoFile.path)")
275
+ var uploadReceivedTraceValues: [String: Any] = [
276
+ "durationMs": Int(Date().timeIntervalSince1970 * 1000 - state.startMs),
277
+ "photoBytes": photoPart.byteCount,
278
+ "contentLength": state.contentLength,
279
+ "method": request.method,
280
+ "path": request.path,
281
+ ]
282
+ if let requestId {
283
+ uploadReceivedTraceValues["requestId"] = requestId
284
+ }
285
+ traceWifiInput("photo_receiver_upload_received", connection: connection, state: state, values: uploadReceivedTraceValues)
286
+ var uploadResponseTraceValues: [String: Any] = [
287
+ "statusCode": 200,
288
+ "success": true,
289
+ "durationMs": Int(Date().timeIntervalSince1970 * 1000 - state.startMs),
290
+ "contentLength": state.contentLength,
291
+ "method": request.method,
292
+ "path": request.path,
293
+ ]
294
+ if let requestId {
295
+ uploadResponseTraceValues["requestId"] = requestId
296
+ }
297
+ traceWifiOutput("photo_receiver_response", connection: connection, state: state, values: uploadResponseTraceValues)
298
+ writeJson(
299
+ connection,
300
+ status: 200,
301
+ body: #"{"ok":true,"requestId":\#(jsonString(requestId ?? "")),"bytes":\#(photoPart.byteCount)}"#
302
+ )
303
+ queue.async { [onUpload] in
304
+ onUpload(upload)
305
+ }
306
+ return true
307
+ }
308
+
309
+ private func appendBodyFragment(_ data: Data, to state: RequestReadState) throws {
310
+ guard !data.isEmpty else {
311
+ return
312
+ }
313
+ let remaining = state.contentLength - state.bodyBytesReceived
314
+ guard remaining > 0 else {
315
+ return
316
+ }
317
+ let acceptedCount = min(data.count, remaining)
318
+ let accepted = Data(data[data.startIndex..<data.startIndex + acceptedCount])
319
+ state.bodyBytesReceived += acceptedCount
320
+ try state.multipartParser?.append(accepted)
321
+ }
322
+
323
+ private func writeJson(_ connection: NWConnection, status: Int, body: String) {
324
+ let reason: String
325
+ switch status {
326
+ case 200:
327
+ reason = "OK"
328
+ case 400:
329
+ reason = "Bad Request"
330
+ case 404:
331
+ reason = "Not Found"
332
+ case 411:
333
+ reason = "Length Required"
334
+ case 413:
335
+ reason = "Payload Too Large"
336
+ default:
337
+ reason = "Internal Server Error"
338
+ }
339
+
340
+ let bodyData = Data(body.utf8)
341
+ let header =
342
+ "HTTP/1.1 \(status) \(reason)\r\n" +
343
+ "Content-Type: application/json\r\n" +
344
+ "Content-Length: \(bodyData.count)\r\n" +
345
+ "Connection: close\r\n" +
346
+ "\r\n"
347
+ var response = Data(header.utf8)
348
+ response.append(bodyData)
349
+ connection.send(content: response, completion: .contentProcessed { _ in
350
+ connection.cancel()
351
+ })
352
+ }
353
+
354
+ private func write(
355
+ _ connection: NWConnection,
356
+ text: String,
357
+ closeAfterSend: Bool
358
+ ) {
359
+ connection.send(content: Data(text.utf8), completion: .contentProcessed { _ in
360
+ if closeAfterSend {
361
+ connection.cancel()
362
+ }
363
+ })
364
+ }
365
+
366
+ private func multipartBoundary(_ contentType: String) -> String? {
367
+ contentType
368
+ .split(separator: ";")
369
+ .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
370
+ .first { $0.caseInsensitiveHasPrefix("boundary=") }?
371
+ .split(separator: "=", maxSplits: 1)
372
+ .last
373
+ .map(String.init)?
374
+ .trimmingCharacters(in: CharacterSet(charactersIn: "\""))
375
+ }
376
+
377
+ private func safeFilePart(_ value: String) -> String {
378
+ let allowed = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-")
379
+ let sanitized = String(value.unicodeScalars.map { allowed.contains($0) ? Character($0) : "_" })
380
+ return sanitized.isEmpty ? "photo" : sanitized
381
+ }
382
+
383
+ private func jsonString(_ value: String) -> String {
384
+ guard
385
+ let data = try? JSONSerialization.data(withJSONObject: [value]),
386
+ let json = String(data: data, encoding: .utf8)
387
+ else {
388
+ return #""""#
389
+ }
390
+ return String(json.dropFirst().dropLast())
391
+ }
392
+
393
+ private func moveFile(_ source: URL, to destination: URL) throws {
394
+ if FileManager.default.fileExists(atPath: destination.path) {
395
+ try FileManager.default.removeItem(at: destination)
396
+ }
397
+ do {
398
+ try FileManager.default.moveItem(at: source, to: destination)
399
+ } catch {
400
+ try FileManager.default.copyItem(at: source, to: destination)
401
+ try? FileManager.default.removeItem(at: source)
402
+ }
403
+ }
404
+
405
+ private func traceWifiInput(
406
+ _ type: String,
407
+ connection: NWConnection,
408
+ state: RequestReadState?,
409
+ values: [String: Any] = [:]
410
+ ) {
411
+ traceWifi(direction: "wifi_to_phone", layer: "wifi_http_input", type: type, connection: connection, state: state, values: values)
412
+ }
413
+
414
+ private func traceWifiOutput(
415
+ _ type: String,
416
+ connection: NWConnection,
417
+ state: RequestReadState?,
418
+ values: [String: Any] = [:]
419
+ ) {
420
+ traceWifi(direction: "phone_to_wifi", layer: "wifi_http_output", type: type, connection: connection, state: state, values: values)
421
+ }
422
+
423
+ private func traceWifi(
424
+ direction: String,
425
+ layer: String,
426
+ type: String,
427
+ connection: NWConnection,
428
+ state: RequestReadState?,
429
+ values: [String: Any]
430
+ ) {
431
+ var payload = endpointPayload(type: type, connection: connection)
432
+ if let state {
433
+ payload["startMs"] = state.startMs
434
+ }
435
+ for (key, value) in values {
436
+ payload[key] = value
437
+ }
438
+ BleTraceLogger.logMap(direction: direction, layer: layer, type: type, payload: payload)
439
+ }
440
+
441
+ private func endpointPayload(type: String, connection: NWConnection) -> [String: Any] {
442
+ var payload: [String: Any] = ["type": type]
443
+ if case let .hostPort(host, port) = connection.endpoint {
444
+ payload["remoteHost"] = String(describing: host)
445
+ payload["remotePort"] = Int(port.rawValue)
446
+ }
447
+ return payload
448
+ }
449
+
450
+ private struct HttpRequest {
451
+ let method: String
452
+ let path: String
453
+ let headers: [String: String]
454
+
455
+ static func parse(_ headerText: String) -> HttpRequest {
456
+ let lines = headerText.components(separatedBy: "\r\n").filter { !$0.isEmpty }
457
+ let requestParts = lines.first?.split(separator: " ").map(String.init) ?? []
458
+ let method = requestParts.first?.uppercased() ?? ""
459
+ let path = requestParts.dropFirst().first?.split(separator: "?").first.map(String.init) ?? ""
460
+ var headers: [String: String] = [:]
461
+ for line in lines.dropFirst() {
462
+ guard let separator = line.firstIndex(of: ":") else {
463
+ continue
464
+ }
465
+ let key = String(line[..<separator].lowercased())
466
+ let value = line[line.index(after: separator)...].trimmingCharacters(in: .whitespaces)
467
+ headers[key] = value
468
+ }
469
+ return HttpRequest(method: method, path: path, headers: headers)
470
+ }
471
+ }
472
+
473
+ private final class MultipartStreamParser {
474
+ private enum Stage {
475
+ case initialBoundary
476
+ case partHeaders
477
+ case partBody
478
+ case boundarySuffix
479
+ case complete
480
+ }
481
+
482
+ private let marker: String
483
+ private let delimiter: Data
484
+ private let crlf = Data([13, 10])
485
+ private let headerDelimiter = Data([13, 10, 13, 10])
486
+ private let uploadDirectory: URL
487
+ private var stage: Stage = .initialBoundary
488
+ private var buffer = Data()
489
+ private var currentPart: PartWriter?
490
+ private var fields: [String: String] = [:]
491
+ private var files: [String: StreamedFile] = [:]
492
+
493
+ var isComplete: Bool {
494
+ stage == .complete
495
+ }
496
+
497
+ init(boundary: String, uploadDirectory: URL) {
498
+ marker = "--\(boundary)"
499
+ delimiter = Data("\r\n--\(boundary)".utf8)
500
+ self.uploadDirectory = uploadDirectory
501
+ }
502
+
503
+ func append(_ data: Data) throws {
504
+ guard !isComplete else {
505
+ return
506
+ }
507
+ buffer.append(data)
508
+ try processBuffer()
509
+ }
510
+
511
+ func parsedMultipart() -> ParsedMultipart? {
512
+ guard isComplete else {
513
+ return nil
514
+ }
515
+ return ParsedMultipart(fields: fields, files: files)
516
+ }
517
+
518
+ func cleanup() {
519
+ currentPart?.cleanup()
520
+ files.values.forEach { try? FileManager.default.removeItem(at: $0.file) }
521
+ }
522
+
523
+ private func processBuffer() throws {
524
+ while true {
525
+ switch stage {
526
+ case .initialBoundary:
527
+ guard let line = try popLine() else {
528
+ return
529
+ }
530
+ if line == "\(marker)--" {
531
+ stage = .complete
532
+ return
533
+ }
534
+ guard line == marker else {
535
+ throw PhotoUploadServerError("multipart_boundary_missing")
536
+ }
537
+ stage = .partHeaders
538
+
539
+ case .partHeaders:
540
+ guard let headerBlock = try popHeaders() else {
541
+ return
542
+ }
543
+ currentPart = try PartWriter(headerBlock: headerBlock, uploadDirectory: uploadDirectory)
544
+ stage = .partBody
545
+
546
+ case .partBody:
547
+ guard let currentPart else {
548
+ throw PhotoUploadServerError("multipart_part_missing")
549
+ }
550
+ if let delimiterRange = buffer.range(of: delimiter) {
551
+ let content = Data(buffer[buffer.startIndex..<delimiterRange.lowerBound])
552
+ try currentPart.write(content)
553
+ buffer.removeSubrange(buffer.startIndex..<delimiterRange.upperBound)
554
+ try currentPart.finish(fields: &fields, files: &files)
555
+ self.currentPart = nil
556
+ stage = .boundarySuffix
557
+ continue
558
+ }
559
+
560
+ let keepBytes = max(delimiter.count - 1, 0)
561
+ if buffer.count > keepBytes {
562
+ let writeCount = buffer.count - keepBytes
563
+ let content = Data(buffer[buffer.startIndex..<buffer.startIndex + writeCount])
564
+ try currentPart.write(content)
565
+ buffer.removeSubrange(buffer.startIndex..<buffer.startIndex + writeCount)
566
+ }
567
+ return
568
+
569
+ case .boundarySuffix:
570
+ guard buffer.count >= 2 else {
571
+ return
572
+ }
573
+ if buffer.starts(with: Data("--".utf8), at: buffer.startIndex) {
574
+ buffer.removeSubrange(buffer.startIndex..<buffer.startIndex + 2)
575
+ stage = .complete
576
+ return
577
+ }
578
+ if buffer.starts(with: crlf, at: buffer.startIndex) {
579
+ buffer.removeSubrange(buffer.startIndex..<buffer.startIndex + crlf.count)
580
+ stage = .partHeaders
581
+ continue
582
+ }
583
+ throw PhotoUploadServerError("invalid multipart boundary suffix")
584
+
585
+ case .complete:
586
+ return
587
+ }
588
+ }
589
+ }
590
+
591
+ private func popLine() throws -> String? {
592
+ guard let lineRange = buffer.range(of: crlf) else {
593
+ if buffer.count > LocalPhotoUploadServer.maxPartHeaderBytes {
594
+ throw PhotoUploadServerError("multipart line too large")
595
+ }
596
+ return nil
597
+ }
598
+ let lineData = Data(buffer[buffer.startIndex..<lineRange.lowerBound])
599
+ buffer.removeSubrange(buffer.startIndex..<lineRange.upperBound)
600
+ return String(data: lineData, encoding: .isoLatin1)
601
+ }
602
+
603
+ private func popHeaders() throws -> String? {
604
+ guard let headerRange = buffer.range(of: headerDelimiter) else {
605
+ if buffer.count > LocalPhotoUploadServer.maxPartHeaderBytes {
606
+ throw PhotoUploadServerError("multipart headers too large")
607
+ }
608
+ return nil
609
+ }
610
+ let headerData = Data(buffer[buffer.startIndex..<headerRange.lowerBound])
611
+ buffer.removeSubrange(buffer.startIndex..<headerRange.upperBound)
612
+ return String(data: headerData, encoding: .isoLatin1)
613
+ }
614
+
615
+ private static func firstCapture(_ pattern: String, in text: String) -> String? {
616
+ guard
617
+ let regex = try? NSRegularExpression(pattern: pattern),
618
+ let match = regex.firstMatch(in: text, range: NSRange(text.startIndex..., in: text)),
619
+ match.numberOfRanges > 1,
620
+ let range = Range(match.range(at: 1), in: text)
621
+ else {
622
+ return nil
623
+ }
624
+ return String(text[range])
625
+ }
626
+
627
+ private final class PartWriter {
628
+ private let name: String?
629
+ private let file: URL?
630
+ private var fileHandle: FileHandle?
631
+ private var fieldData = Data()
632
+ private var byteCount = 0
633
+
634
+ init(headerBlock: String, uploadDirectory: URL) throws {
635
+ let disposition = headerBlock
636
+ .components(separatedBy: "\r\n")
637
+ .first { $0.caseInsensitiveHasPrefix("Content-Disposition:") } ?? ""
638
+ name = MultipartStreamParser.firstCapture(#"name="([^"]+)""#, in: disposition)
639
+ if MultipartStreamParser.firstCapture(#"filename="([^"]*)""#, in: disposition) != nil {
640
+ let tempFile = uploadDirectory
641
+ .appendingPathComponent("upload-\(Int(Date().timeIntervalSince1970 * 1000))-\(UUID().uuidString).tmp")
642
+ FileManager.default.createFile(atPath: tempFile.path, contents: nil)
643
+ file = tempFile
644
+ fileHandle = try FileHandle(forWritingTo: tempFile)
645
+ } else {
646
+ file = nil
647
+ }
648
+ }
649
+
650
+ func write(_ data: Data) throws {
651
+ guard !data.isEmpty else {
652
+ return
653
+ }
654
+ byteCount += data.count
655
+ if let fileHandle {
656
+ fileHandle.write(data)
657
+ } else {
658
+ guard fieldData.count + data.count <= LocalPhotoUploadServer.maxFieldBytes else {
659
+ throw PhotoUploadServerError("multipart field too large")
660
+ }
661
+ fieldData.append(data)
662
+ }
663
+ }
664
+
665
+ func finish(fields: inout [String: String], files: inout [String: StreamedFile]) throws {
666
+ fileHandle?.closeFile()
667
+ fileHandle = nil
668
+ guard let name else {
669
+ cleanup()
670
+ return
671
+ }
672
+ if let file {
673
+ if let previous = files.updateValue(StreamedFile(file: file, byteCount: byteCount), forKey: name) {
674
+ try? FileManager.default.removeItem(at: previous.file)
675
+ }
676
+ } else {
677
+ fields[name] = String(data: fieldData, encoding: .utf8)?
678
+ .trimmingCharacters(in: .whitespacesAndNewlines)
679
+ }
680
+ }
681
+
682
+ func cleanup() {
683
+ fileHandle?.closeFile()
684
+ fileHandle = nil
685
+ if let file {
686
+ try? FileManager.default.removeItem(at: file)
687
+ }
688
+ }
689
+ }
690
+ }
691
+
692
+ private final class RequestReadState {
693
+ let startMs = Date().timeIntervalSince1970 * 1000
694
+ var buffer = Data()
695
+ var request: HttpRequest?
696
+ var contentLength = 0
697
+ var bodyBytesReceived = 0
698
+ var multipartParser: MultipartStreamParser?
699
+ }
700
+
701
+ private struct ParsedMultipart {
702
+ let fields: [String: String]
703
+ let files: [String: StreamedFile]
704
+
705
+ func deleteFiles() {
706
+ files.values.forEach { try? FileManager.default.removeItem(at: $0.file) }
707
+ }
708
+
709
+ func deleteFiles(except keptFile: URL) {
710
+ files.values.forEach { part in
711
+ if part.file.path != keptFile.path {
712
+ try? FileManager.default.removeItem(at: part.file)
713
+ }
714
+ }
715
+ }
716
+ }
717
+
718
+ private struct StreamedFile {
719
+ let file: URL
720
+ let byteCount: Int
721
+ }
722
+
723
+ private struct PhotoUploadServerError: LocalizedError {
724
+ let message: String
725
+
726
+ init(_ message: String) {
727
+ self.message = message
728
+ }
729
+
730
+ var errorDescription: String? {
731
+ message
732
+ }
733
+ }
734
+
735
+ private static let maxHeaderBytes = 64 * 1024
736
+ private static let maxPartHeaderBytes = 64 * 1024
737
+ private static let maxFieldBytes = 64 * 1024
738
+ private static let maxUploadBytes = 25 * 1024 * 1024
739
+ }
740
+
741
+ private extension Data {
742
+ func starts(with prefix: Data, at index: Data.Index) -> Bool {
743
+ guard index >= startIndex, index + prefix.count <= endIndex else {
744
+ return false
745
+ }
746
+ return self[index..<index + prefix.count].elementsEqual(prefix)
747
+ }
748
+ }
749
+
750
+ private extension String {
751
+ func caseInsensitiveHasPrefix(_ prefix: String) -> Bool {
752
+ range(of: prefix, options: [.anchored, .caseInsensitive]) != nil
753
+ }
754
+ }