zmodem.dart 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  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<List<int>> 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 _terminalSink = 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. stdin.add(utf8.encode(input));
  69. }
  70. }
  71. /// This is the entry point of multiplexing, dispatching data to ZModem or
  72. /// terminal depending on the current state.
  73. void _handleStdout(Uint8List chunk) {
  74. if (_session != null) {
  75. _handleZModem(chunk);
  76. return;
  77. }
  78. if (_detectZModem(chunk)) {
  79. return;
  80. }
  81. _terminalSink.add(chunk);
  82. }
  83. /// Detects a ZModem session in [chunk] and starts it if found. Returns true
  84. /// if a session was started.
  85. bool _detectZModem(Uint8List chunk) {
  86. final index = chunk.listIndexOf(_zmodemSenderInit) ??
  87. chunk.listIndexOf(_zmodemReceiverInit);
  88. if (index != null) {
  89. _terminalSink.add(Uint8List.sublistView(chunk, 0, index));
  90. _session = ZModemCore(
  91. onPlainText: (text) {
  92. _terminalSink.add([text]);
  93. },
  94. );
  95. _handleZModem(Uint8List.sublistView(chunk, index));
  96. return true;
  97. }
  98. return false;
  99. }
  100. void _handleZModem(Uint8List chunk) async {
  101. for (final event in _session!.receive(chunk)) {
  102. /// remote is sz
  103. if (event is ZFileOfferedEvent) {
  104. _handleZFileOfferedEvent(event);
  105. } else if (event is ZFileDataEvent) {
  106. _handleZFileDataEvent(event);
  107. } else if (event is ZFileEndEvent) {
  108. await _handleZFileEndEvent(event);
  109. } else if (event is ZSessionFinishedEvent) {
  110. await _handleZSessionFinishedEvent(event);
  111. }
  112. /// remote is rz
  113. else if (event is ZReadyToSendEvent) {
  114. await _handleFileRequestEvent(event);
  115. } else if (event is ZFileAcceptedEvent) {
  116. await _handleFileAcceptedEvent(event);
  117. } else if (event is ZFileSkippedEvent) {
  118. _handleFileSkippedEvent(event);
  119. }
  120. _flush();
  121. }
  122. _flush();
  123. }
  124. void _handleZFileOfferedEvent(ZFileOfferedEvent event) {
  125. final onFileOffer = this.onFileOffer;
  126. if (onFileOffer == null) {
  127. _session!.skipFile();
  128. return;
  129. }
  130. onFileOffer(_createRemoteOffer(event.fileInfo));
  131. }
  132. void _handleZFileDataEvent(ZFileDataEvent event) {
  133. _receiveSink!.add(event.data as Uint8List);
  134. }
  135. Future<void> _handleZFileEndEvent(ZFileEndEvent event) async {
  136. await _closeReceiveSink();
  137. }
  138. Future<void> _handleZSessionFinishedEvent(ZSessionFinishedEvent event) async {
  139. _flush();
  140. await _reset();
  141. }
  142. Future<void> _handleFileRequestEvent(ZReadyToSendEvent event) async {
  143. _fileOffers ??= (await onFileRequest?.call())?.iterator;
  144. _moveToNextOffer();
  145. }
  146. Future<void> _handleFileAcceptedEvent(ZFileAcceptedEvent event) async {
  147. final data = _fileOffers!.current.accept(event.offset);
  148. var bytesSent = 0;
  149. await stdin.addStream(
  150. data.transform(
  151. StreamTransformer<Uint8List, Uint8List>.fromHandlers(
  152. handleData: (chunk, sink) {
  153. bytesSent += chunk.length;
  154. _session!.sendFileData(chunk);
  155. sink.add(_session!.dataToSend());
  156. },
  157. ),
  158. ),
  159. );
  160. _session!.finishSending(event.offset + bytesSent);
  161. }
  162. void _handleFileSkippedEvent(ZFileSkippedEvent event) {
  163. _fileOffers!.current.skip();
  164. _moveToNextOffer();
  165. }
  166. /// Sends next file offer if available, or closes the session if not.
  167. void _moveToNextOffer() {
  168. if (_fileOffers?.moveNext() != true) {
  169. _closeSession();
  170. return;
  171. }
  172. _session!.offerFile(_fileOffers!.current.info);
  173. }
  174. /// Creates a [ZModemOffer] ƒrom the info from remote peer that can be used
  175. /// by local client to accept or skip the file.
  176. ZModemOffer _createRemoteOffer(ZModemFileInfo fileInfo) {
  177. return ZModemCallbackOffer(
  178. fileInfo,
  179. onAccept: (offset) {
  180. _session!.acceptFile(offset);
  181. _flush();
  182. _createReceiveSink();
  183. return _receiveSink!.stream;
  184. },
  185. onSkip: () {
  186. _session!.skipFile();
  187. _flush();
  188. },
  189. );
  190. }
  191. void _createReceiveSink() {
  192. _receiveSink = StreamController<Uint8List>(
  193. onPause: () {
  194. // _stdoutSubscription.pause();
  195. },
  196. onResume: () {
  197. // _stdoutSubscription.resume();
  198. },
  199. );
  200. }
  201. Future<void> _closeReceiveSink() async {
  202. _stdoutSubscription.resume();
  203. await _receiveSink?.close();
  204. _receiveSink = null;
  205. }
  206. /// Requests remote to close the session.
  207. void _closeSession() {
  208. _session!.finishSession();
  209. }
  210. /// Clears all ZModem state.
  211. Future<void> _reset() async {
  212. await _closeReceiveSink();
  213. _fileOffers = null;
  214. _session = null;
  215. }
  216. /// Sends all pending data packets to the remote. No data is automatically
  217. /// sent to the remote without calling this method.
  218. void _flush() {
  219. final dataToSend = _session?.dataToSend();
  220. if (dataToSend != null && dataToSend.isNotEmpty) {
  221. stdin.add(dataToSend);
  222. }
  223. }
  224. }
  225. extension ListExtension on List<int> {
  226. String dump() {
  227. return map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ');
  228. }
  229. int? listIndexOf(List<int> other, [int start = 0]) {
  230. if (other.length + start > length) {
  231. return null;
  232. }
  233. for (var i = start; i < length - other.length; i++) {
  234. if (this[i] == other[0]) {
  235. var found = true;
  236. for (var j = 1; j < other.length; j++) {
  237. if (this[i + j] != other[j]) {
  238. found = false;
  239. break;
  240. }
  241. }
  242. if (found) {
  243. return i;
  244. }
  245. }
  246. }
  247. return null;
  248. }
  249. }