WIP X-plore j 보충: SMB 찾기에 mDNS (_smb._tcp.local) 를 먼저 (검토 대기)
관리자 지시 (10/18): 훑기보다 먼저 mDNS 로 빠르게 찾기. - lib/core/mdns_smb.dart (새, 패키지 없음): "레거시 유니캐스트" 질의 (RFC 6762 6.7) - 빈 포트에서 224.0.0.251:5353 으로 PTR (QU) 을 묻고 응답자가 그 포트로 바로 답한다. Android 의 멀티캐스트 받기 (MulticastLock, Kotlin) 없이도 받을 수 있게 고른 방식 (내 판단). PTR → 인스턴스, SRV → 호스트 이름, A → IPv4 (이름 압축 읽기). 1.5초 모으고, 못 찾으면 빈 목록 (오류 없음). 묻기만 하고 알리지 않음 - 찾기 창: mDNS 먼저 → 이어서 포트 훑기 (mDNS 를 안 하는 NAS 도). 같은 주소는 하나로 (이름은 mDNS 것), 주소 순서. 안내 글도 고침 시험: mdns_smb_test 3 (질의 꼴 · 손으로 만든 응답 읽기 - 압축 이름 · SRV · A · 합치기) · smb_find_test - 4 통과, analyze 문제 없음, l10n 빠짐 0. 실제 NAS 의 mDNS 응답은 기기 · 사무실 망에서 확인 필요 (Synology 는 기본으로 _smb._tcp 를 알림). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>hint_fix
parent
3a1a6391ca
commit
88379f16f4
|
|
@ -1101,6 +1101,7 @@
|
||||||
"매번 고르기": "Ask every time",
|
"매번 고르기": "Ask every time",
|
||||||
"매번 묻기 (기본)": "Ask every time (default)",
|
"매번 묻기 (기본)": "Ask every time (default)",
|
||||||
"매일 종일": "Every day, all day",
|
"매일 종일": "Every day, all day",
|
||||||
|
"먼저 mDNS 로 묻고 (이름을 알리는 NAS), 이어서 이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 파일 · 계정 정보는 보내지 않습니다.": "First asks via mDNS (NAS devices that announce their names), then tries an SMB (445) connection to devices on the network this device is on (Wi-Fi · wired) and closes it right away. No files or account details are sent.",
|
||||||
"멈추고 옮기기": "Stop and move",
|
"멈추고 옮기기": "Stop and move",
|
||||||
"멈춤": "Stopped",
|
"멈춤": "Stopped",
|
||||||
"멈춤 (이번 실행 동안)": "Pause (this session)",
|
"멈춤 (이번 실행 동안)": "Pause (this session)",
|
||||||
|
|
@ -1838,7 +1839,6 @@
|
||||||
"이 기기 (SD1.5 + LCM)": "This device (SD1.5 + LCM)",
|
"이 기기 (SD1.5 + LCM)": "This device (SD1.5 + LCM)",
|
||||||
"이 기기 ({0})": "This device ({0})",
|
"이 기기 ({0})": "This device ({0})",
|
||||||
"이 기기 메모리 {0}GB 기준:": "For this device's {0}GB of memory:",
|
"이 기기 메모리 {0}GB 기준:": "For this device's {0}GB of memory:",
|
||||||
"이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 아무것도 보내지 않습니다.": "Tries an SMB (445) connection to devices on the network this device is on (Wi-Fi · wired) and closes it right away. Nothing is sent.",
|
|
||||||
"이 기기에는 원문이 아니라 되돌릴 수 없는 해시만 남습니다.": "Only an irreversible hash is stored on this device, not the password itself.",
|
"이 기기에는 원문이 아니라 되돌릴 수 없는 해시만 남습니다.": "Only an irreversible hash is stored on this device, not the password itself.",
|
||||||
"이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "About {0} per image on this device (based on last time)",
|
"이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "About {0} per image on this device (based on last time)",
|
||||||
"이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "About {0} per image, {1} in total on this device (based on last time)",
|
"이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "About {0} per image, {1} in total on this device (based on last time)",
|
||||||
|
|
|
||||||
|
|
@ -1101,6 +1101,7 @@
|
||||||
"매번 고르기": "毎回選ぶ",
|
"매번 고르기": "毎回選ぶ",
|
||||||
"매번 묻기 (기본)": "毎回確認 (既定)",
|
"매번 묻기 (기본)": "毎回確認 (既定)",
|
||||||
"매일 종일": "毎日 終日",
|
"매일 종일": "毎日 終日",
|
||||||
|
"먼저 mDNS 로 묻고 (이름을 알리는 NAS), 이어서 이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 파일 · 계정 정보는 보내지 않습니다.": "まず mDNS で問い合わせ (名前を知らせる NAS)、続いてこの端末が接続しているネットワーク (Wi-Fi · 有線) の機器に SMB (445) で接続だけ試し、すぐに閉じます。ファイルやアカウント情報は送りません。",
|
||||||
"멈추고 옮기기": "止めて移す",
|
"멈추고 옮기기": "止めて移す",
|
||||||
"멈춤": "停止",
|
"멈춤": "停止",
|
||||||
"멈춤 (이번 실행 동안)": "停止 (今回の実行中)",
|
"멈춤 (이번 실행 동안)": "停止 (今回の実行中)",
|
||||||
|
|
@ -1838,7 +1839,6 @@
|
||||||
"이 기기 (SD1.5 + LCM)": "この端末 (SD1.5 + LCM)",
|
"이 기기 (SD1.5 + LCM)": "この端末 (SD1.5 + LCM)",
|
||||||
"이 기기 ({0})": "この端末 ({0})",
|
"이 기기 ({0})": "この端末 ({0})",
|
||||||
"이 기기 메모리 {0}GB 기준:": "この端末のメモリ {0}GB の場合:",
|
"이 기기 메모리 {0}GB 기준:": "この端末のメモリ {0}GB の場合:",
|
||||||
"이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 아무것도 보내지 않습니다.": "この端末が接続しているネットワーク (Wi-Fi · 有線) の機器に SMB (445) で接続だけ試し、すぐに閉じます。何も送りません。",
|
|
||||||
"이 기기에는 원문이 아니라 되돌릴 수 없는 해시만 남습니다.": "この端末にはパスワード自体ではなく、元に戻せないハッシュだけが残ります。",
|
"이 기기에는 원문이 아니라 되돌릴 수 없는 해시만 남습니다.": "この端末にはパスワード自体ではなく、元に戻せないハッシュだけが残ります。",
|
||||||
"이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "この端末で 1 枚約 {0} (前回の所要時間から)",
|
"이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "この端末で 1 枚約 {0} (前回の所要時間から)",
|
||||||
"이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "この端末で 1 枚約 {0}・全部で約 {1} (前回の所要時間から)",
|
"이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "この端末で 1 枚約 {0}・全部で約 {1} (前回の所要時間から)",
|
||||||
|
|
|
||||||
|
|
@ -1101,6 +1101,7 @@
|
||||||
"매번 고르기": "每次选择",
|
"매번 고르기": "每次选择",
|
||||||
"매번 묻기 (기본)": "每次询问 (默认)",
|
"매번 묻기 (기본)": "每次询问 (默认)",
|
||||||
"매일 종일": "每天 全天",
|
"매일 종일": "每天 全天",
|
||||||
|
"먼저 mDNS 로 묻고 (이름을 알리는 NAS), 이어서 이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 파일 · 계정 정보는 보내지 않습니다.": "先通过 mDNS 询问 (会公布名称的 NAS),再仅尝试与本设备所在网络 (Wi-Fi · 有线) 中的设备建立 SMB (445) 连接并立即关闭。不会发送文件或账户信息。",
|
||||||
"멈추고 옮기기": "停止并移动",
|
"멈추고 옮기기": "停止并移动",
|
||||||
"멈춤": "已停止",
|
"멈춤": "已停止",
|
||||||
"멈춤 (이번 실행 동안)": "暂停 (本次运行期间)",
|
"멈춤 (이번 실행 동안)": "暂停 (本次运行期间)",
|
||||||
|
|
@ -1838,7 +1839,6 @@
|
||||||
"이 기기 (SD1.5 + LCM)": "本设备 (SD1.5 + LCM)",
|
"이 기기 (SD1.5 + LCM)": "本设备 (SD1.5 + LCM)",
|
||||||
"이 기기 ({0})": "本设备 ({0})",
|
"이 기기 ({0})": "本设备 ({0})",
|
||||||
"이 기기 메모리 {0}GB 기준:": "按本设备内存 {0}GB:",
|
"이 기기 메모리 {0}GB 기준:": "按本设备内存 {0}GB:",
|
||||||
"이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 아무것도 보내지 않습니다.": "仅尝试与本设备所在网络 (Wi-Fi · 有线) 中的设备建立 SMB (445) 连接并立即关闭。不会发送任何内容。",
|
|
||||||
"이 기기에는 원문이 아니라 되돌릴 수 없는 해시만 남습니다.": "本设备只保存无法还原的哈希值,而不是密码本身。",
|
"이 기기에는 원문이 아니라 되돌릴 수 없는 해시만 남습니다.": "本设备只保存无法还原的哈希值,而不是密码本身。",
|
||||||
"이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "本设备每张约 {0} (按上次耗时)",
|
"이 기기에서 한 장 약 {0} (지난번 걸린 시간 기준)": "本设备每张约 {0} (按上次耗时)",
|
||||||
"이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "本设备每张约 {0} · 共约 {1} (按上次耗时)",
|
"이 기기에서 한 장 약 {0} · 모두 약 {1} (지난번 걸린 시간 기준)": "本设备每张约 {0} · 共约 {1} (按上次耗时)",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'lan_scan.dart' show LanHost;
|
||||||
|
|
||||||
|
/// X-plore j 보충 (관리자 지시 10/18): mDNS 로 SMB 서버 찾기 (_smb._tcp.local) - 포트 훑기보다 먼저, 빠르게 (1~2초).
|
||||||
|
/// "레거시 유니캐스트" 질의 (RFC 6762 6.7): 5353 이 아닌 빈 포트에서 224.0.0.251:5353 으로 묻고, 응답자가 그 포트로 바로 답한다 -
|
||||||
|
/// Android 에서 멀티캐스트 받기 (MulticastLock) 없이도 받을 수 있다. 묻기만 하고 아무것도 알리지 않는다.
|
||||||
|
/// 돌려주는 것: 서버 이름 (SRV 의 대상 · 없으면 인스턴스 이름) 과 IPv4 주소. 못 찾으면 빈 목록 (오류를 내지 않음)
|
||||||
|
Future<List<LanHost>> findSmbServersMdns({Duration wait = const Duration(milliseconds: 1500), bool Function()? cancelled}) async {
|
||||||
|
RawDatagramSocket? sock;
|
||||||
|
try {
|
||||||
|
sock = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
|
||||||
|
} catch (_) {
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
final id = Random().nextInt(0xffff);
|
||||||
|
final ptr = <String>{}; // 인스턴스 이름들
|
||||||
|
final srv = <String, String>{}; // 인스턴스 → 대상 호스트 이름
|
||||||
|
final a = <String, String>{}; // 호스트 이름 → IPv4
|
||||||
|
final from = <String, String>{}; // 인스턴스 → 응답을 보낸 주소 (A 가 없을 때)
|
||||||
|
final done = Completer<void>();
|
||||||
|
final sub = sock.listen((ev) {
|
||||||
|
if (ev != RawSocketEvent.read) return;
|
||||||
|
final d = sock!.receive();
|
||||||
|
if (d == null) return;
|
||||||
|
try {
|
||||||
|
for (final r in parseMdns(d.data)) {
|
||||||
|
switch (r.type) {
|
||||||
|
case 12 when r.name.toLowerCase() == '_smb._tcp.local':
|
||||||
|
ptr.add(r.data);
|
||||||
|
from[r.data] = d.address.address;
|
||||||
|
case 33:
|
||||||
|
srv[r.name] = r.data;
|
||||||
|
case 1:
|
||||||
|
a[r.name.toLowerCase()] = r.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
});
|
||||||
|
sock.send(mdnsQuery(id, '_smb._tcp.local', 12), InternetAddress('224.0.0.251'), 5353);
|
||||||
|
// 잠깐 기다리며 모은다 (취소되면 바로)
|
||||||
|
final end = DateTime.now().add(wait);
|
||||||
|
while (DateTime.now().isBefore(end) && !(cancelled?.call() ?? false)) {
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||||
|
}
|
||||||
|
if (!done.isCompleted) done.complete();
|
||||||
|
await sub.cancel();
|
||||||
|
sock.close();
|
||||||
|
final out = <LanHost>[];
|
||||||
|
final seen = <String>{};
|
||||||
|
for (final inst in ptr) {
|
||||||
|
final host = srv[inst];
|
||||||
|
final ip = (host == null ? null : a[host.toLowerCase()]) ?? from[inst];
|
||||||
|
if (ip == null || !seen.add(ip)) continue;
|
||||||
|
final label = host ?? inst.replaceFirst(RegExp(r'\._smb\._tcp\.local$', caseSensitive: false), '');
|
||||||
|
out.add(LanHost(ip, label.endsWith('.') ? label.substring(0, label.length - 1) : label));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// mDNS 질의 하나 (QU 비트 - 유니캐스트로 답해 달라)
|
||||||
|
Uint8List mdnsQuery(int id, String name, int type) {
|
||||||
|
final b = BytesBuilder();
|
||||||
|
b.add([id >> 8, id & 0xff, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]);
|
||||||
|
for (final part in name.split('.')) {
|
||||||
|
final x = utf8.encode(part);
|
||||||
|
b.addByte(x.length);
|
||||||
|
b.add(x);
|
||||||
|
}
|
||||||
|
b.addByte(0);
|
||||||
|
b.add([type >> 8, type & 0xff, 0x80, 0x01]); // 형식 · IN + QU
|
||||||
|
return b.toBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 응답의 기록 (답 · 권한 · 추가 모두): (이름, 형식, 자료 - PTR/SRV 는 이름, A 는 주소)
|
||||||
|
List<({String name, int type, String data})> parseMdns(Uint8List m) {
|
||||||
|
final v = ByteData.sublistView(m);
|
||||||
|
int u16(int o) => v.getUint16(o);
|
||||||
|
String readName(int o, List<int> next) {
|
||||||
|
final parts = <String>[];
|
||||||
|
var jumped = false;
|
||||||
|
var guard = 0;
|
||||||
|
while (guard++ < 64) {
|
||||||
|
final len = m[o];
|
||||||
|
if (len == 0) {
|
||||||
|
o += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (len & 0xc0 == 0xc0) {
|
||||||
|
final ptr = ((len & 0x3f) << 8) | m[o + 1];
|
||||||
|
if (!jumped) next.add(o + 2);
|
||||||
|
jumped = true;
|
||||||
|
o = ptr;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
parts.add(utf8.decode(m.sublist(o + 1, o + 1 + len), allowMalformed: true));
|
||||||
|
o += 1 + len;
|
||||||
|
}
|
||||||
|
if (!jumped) next.add(o);
|
||||||
|
return parts.join('.');
|
||||||
|
}
|
||||||
|
|
||||||
|
final qd = u16(4), an = u16(6), ns = u16(8), ar = u16(10);
|
||||||
|
var o = 12;
|
||||||
|
for (var i = 0; i < qd; i++) {
|
||||||
|
final n = <int>[];
|
||||||
|
readName(o, n);
|
||||||
|
o = n.first + 4;
|
||||||
|
}
|
||||||
|
final out = <({String name, int type, String data})>[];
|
||||||
|
for (var i = 0; i < an + ns + ar; i++) {
|
||||||
|
final n = <int>[];
|
||||||
|
final name = readName(o, n);
|
||||||
|
o = n.first;
|
||||||
|
final type = u16(o), len = u16(o + 8);
|
||||||
|
final rd = o + 10;
|
||||||
|
String data = '';
|
||||||
|
if (type == 12) {
|
||||||
|
data = readName(rd, []);
|
||||||
|
} else if (type == 33 && len > 6) {
|
||||||
|
data = readName(rd + 6, []);
|
||||||
|
} else if (type == 1 && len == 4) {
|
||||||
|
data = '${m[rd]}.${m[rd + 1]}.${m[rd + 2]}.${m[rd + 3]}';
|
||||||
|
}
|
||||||
|
out.add((name: name, type: type, data: data));
|
||||||
|
o = rd + len;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 찾기 창의 기본: mDNS 를 먼저 (빠르게 · 이름까지), 이어서 포트 훑기 (mDNS 를 안 하는 NAS 도). 같은 주소는 하나로 (이름은 mDNS 것)
|
||||||
|
Future<List<LanHost>> findSmbServersBoth(
|
||||||
|
Future<List<LanHost>> Function({bool Function()? cancelled, void Function(int done, int total)? onProgress}) scan,
|
||||||
|
{bool Function()? cancelled, void Function(int done, int total)? onProgress}) async {
|
||||||
|
final m = await findSmbServersMdns(cancelled: cancelled);
|
||||||
|
if (cancelled?.call() ?? false) return m;
|
||||||
|
final s = await scan(cancelled: cancelled, onProgress: onProgress);
|
||||||
|
final byIp = {for (final h in s) h.address: h};
|
||||||
|
for (final h in m) {
|
||||||
|
byIp[h.address] = h;
|
||||||
|
}
|
||||||
|
final out = byIp.values.toList()
|
||||||
|
..sort((a, b) {
|
||||||
|
int key(String ip) => ip.split('.').fold(0, (x, y) => x * 256 + (int.tryParse(y) ?? 0));
|
||||||
|
return key(a.address).compareTo(key(b.address));
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../app/app_controller.dart';
|
import '../app/app_controller.dart';
|
||||||
import '../core/lan_scan.dart';
|
import '../core/lan_scan.dart';
|
||||||
|
import '../core/mdns_smb.dart';
|
||||||
import '../core/secret_gate.dart';
|
import '../core/secret_gate.dart';
|
||||||
import '../core/smb.dart';
|
import '../core/smb.dart';
|
||||||
import '../l10n/tr.dart';
|
import '../l10n/tr.dart';
|
||||||
|
|
@ -273,7 +274,8 @@ Future<List<LanHost>> Function({bool Function()? cancelled, void Function(int do
|
||||||
/// X-plore j: 같은 망의 SMB 서버를 찾아 고르는 창 (진행 · 취소). 이 기기의 사설망 (/24) 의 445 포트에 연결만 해 보고 바로 닫는다 (보내는 것 없음)
|
/// X-plore j: 같은 망의 SMB 서버를 찾아 고르는 창 (진행 · 취소). 이 기기의 사설망 (/24) 의 445 포트에 연결만 해 보고 바로 닫는다 (보내는 것 없음)
|
||||||
/// [find]: 시험에서 바꿔 끼움 (실제 망을 훑지 않게)
|
/// [find]: 시험에서 바꿔 끼움 (실제 망을 훑지 않게)
|
||||||
Future<LanHost?> pickLanSmbServer(BuildContext context) async {
|
Future<LanHost?> pickLanSmbServer(BuildContext context) async {
|
||||||
final find = debugFindSmbServers ?? findSmbServers;
|
final find = debugFindSmbServers ?? ({bool Function()? cancelled, void Function(int done, int total)? onProgress}) =>
|
||||||
|
findSmbServersBoth(findSmbServers, cancelled: cancelled, onProgress: onProgress);
|
||||||
var cancelled = false;
|
var cancelled = false;
|
||||||
var done = 0, total = 0;
|
var done = 0, total = 0;
|
||||||
List<LanHost>? found;
|
List<LanHost>? found;
|
||||||
|
|
@ -301,7 +303,7 @@ Future<LanHost?> pickLanSmbServer(BuildContext context) async {
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: 460,
|
width: 460,
|
||||||
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
Text(tr('이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 아무것도 보내지 않습니다.'),
|
Text(tr('먼저 mDNS 로 묻고 (이름을 알리는 NAS), 이어서 이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 파일 · 계정 정보는 보내지 않습니다.'),
|
||||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
if (list == null && error == null) ...[
|
if (list == null && error == null) ...[
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jj_mkvmaker/core/lan_scan.dart';
|
||||||
|
import 'package:jj_mkvmaker/core/mdns_smb.dart';
|
||||||
|
|
||||||
|
/// X-plore j 보충: mDNS (_smb._tcp.local) 질의 · 응답 읽기 (이름 압축 포함), 찾기 합치기
|
||||||
|
void main() {
|
||||||
|
List<int> name(String n) => [
|
||||||
|
for (final p in n.split('.')) ...[p.length, ...p.codeUnits],
|
||||||
|
0,
|
||||||
|
];
|
||||||
|
|
||||||
|
test('질의: PTR · QU (유니캐스트로 답해 달라)', () {
|
||||||
|
final q = mdnsQuery(0x1234, '_smb._tcp.local', 12);
|
||||||
|
expect(q.sublist(0, 6), [0x12, 0x34, 0, 0, 0, 1]);
|
||||||
|
expect(q.sublist(q.length - 4), [0, 12, 0x80, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('응답: PTR → 인스턴스, SRV → 호스트 (이름 압축), A → 주소', () {
|
||||||
|
final b = BytesBuilder();
|
||||||
|
b.add([0, 0, 0x84, 0, 0, 0, 0, 1, 0, 0, 0, 2]); // 답 1 · 추가 2
|
||||||
|
final ptrOwner = b.length;
|
||||||
|
b.add(name('_smb._tcp.local'));
|
||||||
|
b.add([0, 12, 0, 1, 0, 0, 0, 120]);
|
||||||
|
final inst = [3, ...'NAS'.codeUnits, 0xc0, ptrOwner]; // "NAS" + 압축 (_smb._tcp.local)
|
||||||
|
b.add([0, inst.length, ...inst]);
|
||||||
|
final instAt = b.length - inst.length;
|
||||||
|
// SRV: 이름 = 인스턴스 (압축), 자료 = 우선 · 무게 · 포트 · 대상 "ds1815.local"
|
||||||
|
b.add([0xc0, instAt, 0, 33, 0, 1, 0, 0, 0, 120]);
|
||||||
|
final target = name('ds1815.local');
|
||||||
|
b.add([0, 6 + target.length, 0, 0, 0, 0, 1, 189, ...target]);
|
||||||
|
final targetAt = b.length - target.length;
|
||||||
|
b.add([0xc0, targetAt, 0, 1, 0, 1, 0, 0, 0, 120, 0, 4, 192, 168, 0, 15]);
|
||||||
|
final r = parseMdns(Uint8List.fromList(b.toBytes()));
|
||||||
|
expect(r.map((x) => (x.name, x.type, x.data)), [
|
||||||
|
('_smb._tcp.local', 12, 'NAS._smb._tcp.local'),
|
||||||
|
('NAS._smb._tcp.local', 33, 'ds1815.local'),
|
||||||
|
('ds1815.local', 1, '192.168.0.15'),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('합치기: mDNS 이름이 포트 훑기 결과를 덮고, 주소 순서대로', () async {
|
||||||
|
final out = await findSmbServersBoth(({cancelled, onProgress}) async => const [LanHost('192.168.0.20', null), LanHost('192.168.0.3', null)],
|
||||||
|
cancelled: () => false);
|
||||||
|
// 이 PC 의 망에 실제 mDNS 응답자가 있으면 더 나올 수 있다 - 훑기 결과 두 개는 늘 있어야 함
|
||||||
|
expect(out.map((h) => h.address), containsAllInOrder(['192.168.0.3', '192.168.0.20']));
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue