zmodem.dart 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:typed_data';
  4. import 'package:zmodem/zmodem.dart';
  5. export 'package:zmodem/zmodem.dart' show ZModemFileInfo;
  6. typedef ZModemInputHandler = void Function(String output);
  7. typedef ZModemOfferHandler = void Function(ZModemOffer offer);
  8. typedef ZModemRequestHandler = Future<Iterable<ZModemOffer>> Function();
  9. abstract class ZModemOffer {
  10. ZModemFileInfo get info;
  11. Stream<Uint8List> accept(int offset);
  12. void skip();
  13. }
  14. class ZModemCallbackOffer implements ZModemOffer {
  15. @override
  16. final ZModemFileInfo info;
  17. final Stream<Uint8List> Function(int offset) onAccept;
  18. final void Function()? onSkip;
  19. ZModemCallbackOffer(this.info, {required this.onAccept, this.onSkip});
  20. @override
  21. Stream<Uint8List> accept(int offset) {
  22. return onAccept(offset);
  23. }
  24. @override
  25. void skip() {
  26. onSkip?.call();
  27. }
  28. }
  29. final _zmodemSenderInit = '**\x18B0000000'.codeUnits;
  30. final _zmodemReceiverInit = '**\x18B0100000'.codeUnits;
  31. class ZModemMux {
  32. /// Data from the underlying data channel.
  33. final Stream<Uint8List> stdout;
  34. /// The sink to write data to the underlying data channel.
  35. final StreamSink stdin;
  36. /// The callback to receive data that should be written to the terminal.
  37. ZModemInputHandler? onTerminalInput;
  38. /// The callback to handle file receiving. If not set, all offers will be
  39. /// skipped.
  40. ZModemOfferHandler? onFileOffer;
  41. /// The callback to handle file sending. If not set, all requests will be
  42. /// ignored.
  43. ZModemRequestHandler? onFileRequest;
  44. ZModemMux({required this.stdin, required this.stdout}) {
  45. _stdoutSubscription = stdout.listen(_handleStdout);
  46. }
  47. /// Subscriptions to [stdout]. Used to pause/resume the stream when no more
  48. /// space is available in local buffers.
  49. late final StreamSubscription<Uint8List> _stdoutSubscription;
  50. late final _terminalInputSink = StreamController<List<int>>(
  51. // onPause: _stdoutSubscription.pause,
  52. // onResume: _stdoutSubscription.resume,
  53. )
  54. ..stream
  55. .transform(Utf8Decoder(allowMalformed: true))
  56. .listen(onTerminalInput);
  57. /// Current ZModem session. If null, no session is active.
  58. ZModemCore? _session;
  59. /// The sink to write data when receiving a file. If null, no file is being
  60. /// received.
  61. StreamController<Uint8List>? _receiveSink;
  62. /// Offers to send to the remote peer. If null, no offers are being sent.
  63. Iterator<ZModemOffer>? _fileOffers;
  64. /// Writes terminal output to the underlying connection. [input] may be
  65. /// buffered if a ZModem session is active.
  66. void terminalWrite(String input) {
  67. if (_session == null) {
  68. print('-->t: ${input}');
  69. stdin.add(utf8.encode(input) as Uint8List);
  70. }
  71. }
  72. /// This is the entry point of multiplexing, dispatching data to ZModem or
  73. /// terminal depending on the current state.
  74. void _handleStdout(Uint8List chunk) {
  75. print('<--: ${chunk.dump()}');
  76. if (_session != null) {
  77. _handleZModem(chunk);
  78. return;
  79. }
  80. if (_detectZModem(chunk)) {
  81. return;
  82. }
  83. _terminalInputSink.add(chunk);
  84. }
  85. /// Detects a ZModem session in [chunk] and starts it if found. Returns true
  86. /// if a session was started.
  87. bool _detectZModem(Uint8List chunk) {
  88. final index = chunk.listIndexOf(_zmodemSenderInit) ??
  89. chunk.listIndexOf(_zmodemReceiverInit);
  90. if (index != null) {
  91. _terminalInputSink.add(Uint8List.sublistView(chunk, 0, index));
  92. _session = ZModemCore(
  93. onTrace: print,
  94. onPlainText: (text) {
  95. _terminalInputSink.add([text]);
  96. },
  97. );
  98. _handleZModem(Uint8List.sublistView(chunk, index));
  99. return true;
  100. }
  101. return false;
  102. }
  103. void _handleZModem(Uint8List chunk) {
  104. print('_handleZModem');
  105. // print('bytes: ${chunk.map((e) => e.toRadixString(16)).toList()}');
  106. for (final event in _session!.receive(chunk)) {
  107. print('event: $event');
  108. /// remote is sz
  109. if (event is ZFileOfferedEvent) {
  110. _handleZFileOfferedEvent(event);
  111. } else if (event is ZFileDataEvent) {
  112. _handleZFileDataEvent(event);
  113. } else if (event is ZFileEndEvent) {
  114. _handleZFileEndEvent(event);
  115. } else if (event is ZSessionFinishedEvent) {
  116. _handleZSessionFinishedEvent(event);
  117. }
  118. /// remote is rz
  119. else if (event is ZReadyToSendEvent) {
  120. _handleFileRequestEvent(event);
  121. } else if (event is ZFileAcceptedEvent) {
  122. _handleFileAcceptedEvent(event);
  123. } else if (event is ZFileSkippedEvent) {
  124. _handleFileSkippedEvent(event);
  125. }
  126. }
  127. _flush();
  128. }
  129. void _handleZFileOfferedEvent(ZFileOfferedEvent event) {
  130. final onFileOffer = this.onFileOffer;
  131. if (onFileOffer == null) {
  132. _session!.skipFile();
  133. _flush();
  134. return;
  135. }
  136. onFileOffer(_createRemoteOffer(event.fileInfo));
  137. }
  138. void _handleZFileDataEvent(ZFileDataEvent event) {
  139. _receiveSink!.add(event.data as Uint8List);
  140. }
  141. void _handleZFileEndEvent(ZFileEndEvent event) async {
  142. await _closeReceiveSink();
  143. }
  144. void _handleZSessionFinishedEvent(ZSessionFinishedEvent event) async {
  145. await _reset();
  146. }
  147. void _handleFileRequestEvent(ZReadyToSendEvent event) async {
  148. _fileOffers ??= (await onFileRequest?.call())?.iterator;
  149. _moveToNextOffer();
  150. }
  151. void _handleFileAcceptedEvent(ZFileAcceptedEvent event) async {
  152. final data = _fileOffers!.current.accept(event.offset);
  153. var bytesSent = 0;
  154. final subscription = data.listen(
  155. (chunk) {
  156. bytesSent += chunk.length;
  157. print('bytesSent: $bytesSent');
  158. _session!.sendFileData(chunk);
  159. _flush();
  160. },
  161. onDone: () {
  162. print('bytesSent fin: $bytesSent');
  163. _session!.finishSending(event.offset + bytesSent);
  164. _flush();
  165. },
  166. );
  167. }
  168. void _handleFileSkippedEvent(ZFileSkippedEvent event) {
  169. _fileOffers!.current.skip();
  170. _moveToNextOffer();
  171. }
  172. /// Sends next file offer if available, or closes the session if not.
  173. void _moveToNextOffer() {
  174. print('_offerNextFileIfNeeded');
  175. if (_fileOffers?.moveNext() != true) {
  176. print('no more files');
  177. _closeSession();
  178. return;
  179. }
  180. _session!.offerFile(_fileOffers!.current.info);
  181. _flush();
  182. }
  183. /// Creates a [ZModemOffer] ƒrom the info from remote peer that can be used
  184. /// by local client to accept or skip the file.
  185. ZModemOffer _createRemoteOffer(ZModemFileInfo fileInfo) {
  186. return ZModemCallbackOffer(
  187. fileInfo,
  188. onAccept: (offset) {
  189. _session!.acceptFile(offset);
  190. _flush();
  191. _createReceiveSink();
  192. return _receiveSink!.stream;
  193. },
  194. onSkip: () {
  195. _session!.skipFile();
  196. _flush();
  197. },
  198. );
  199. }
  200. void _createReceiveSink() {
  201. _receiveSink = StreamController<Uint8List>(
  202. // onPause: _stdoutSubscription.pause,
  203. onResume: _stdoutSubscription.resume,
  204. );
  205. }
  206. Future<void> _closeReceiveSink() async {
  207. await _receiveSink?.close();
  208. _receiveSink = null;
  209. }
  210. /// Requests remote to close the session.
  211. Future<void> _closeSession() async {
  212. _session!.finishSession();
  213. _flush();
  214. }
  215. /// Clears all ZModem state.
  216. Future<void> _reset() async {
  217. await _closeReceiveSink();
  218. _fileOffers = null;
  219. _session = null;
  220. }
  221. /// Sends all pending data packets to the remote. No data is automatically
  222. /// sent to the remote without calling this method.
  223. void _flush() {
  224. final dataToSend = _session!.dataToSend();
  225. if (dataToSend.isNotEmpty) {
  226. // print('-->: ${dataToSend.dump()}');
  227. stdin.add(dataToSend);
  228. }
  229. }
  230. }
  231. extension on List<int> {
  232. String dump() {
  233. return map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ');
  234. }
  235. int? listIndexOf(List<int> other, [int start = 0]) {
  236. if (other.length + start > length) {
  237. return null;
  238. }
  239. for (var i = start; i < length - other.length; i++) {
  240. if (this[i] == other[0]) {
  241. var found = true;
  242. for (var j = 1; j < other.length; j++) {
  243. if (this[i + j] != other[j]) {
  244. found = false;
  245. break;
  246. }
  247. }
  248. if (found) {
  249. return i;
  250. }
  251. }
  252. }
  253. return null;
  254. }
  255. }