1 /*
2 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
3 SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
4 */
5
6 #include <QCoreApplication>
7 #include <QUrlQuery>
8 #include <QScopeGuard>
9
10 #include "smbcdiscoverer.h"
11
12 static QEvent::Type LoopEvent = QEvent::User;
13
14 class SMBCServerDiscovery : public SMBCDiscovery
15 {
16 public:
SMBCServerDiscovery(const UDSEntry & entry)17 SMBCServerDiscovery(const UDSEntry &entry)
18 : SMBCDiscovery(entry)
19 {
20 m_entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
21 m_entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH));
22 m_entry.fastInsert(KIO::UDSEntry::UDS_URL, url());
23 m_entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("application/x-smb-server"));
24 m_entry.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, QStringLiteral("network-server"));
25 }
26
url()27 QString url()
28 {
29 QUrl u("smb://");
30 u.setHost(udsName());
31 return u.url();
32 }
33 };
34
35 class SMBCShareDiscovery : public SMBCDiscovery
36 {
37 public:
SMBCShareDiscovery(const UDSEntry & entry)38 SMBCShareDiscovery(const UDSEntry &entry)
39 : SMBCDiscovery(entry)
40 {
41 m_entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
42 m_entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH));
43 }
44 };
45
46 class SMBCWorkgroupDiscovery : public SMBCDiscovery
47 {
48 public:
SMBCWorkgroupDiscovery(const UDSEntry & entry)49 SMBCWorkgroupDiscovery(const UDSEntry &entry)
50 : SMBCDiscovery(entry)
51 {
52 m_entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
53 m_entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, (S_IRUSR | S_IRGRP | S_IROTH | S_IXUSR | S_IXGRP | S_IXOTH));
54 m_entry.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("application/x-smb-workgroup"));
55 m_entry.fastInsert(KIO::UDSEntry::UDS_URL, url());
56 }
57
url()58 QString url()
59 {
60 QUrl u("smb://");
61 u.setHost(udsName());
62 if (!u.isValid()) {
63 // In the event that the workgroup contains bad characters, put it in a query instead.
64 // This is transparently handled by SMBUrl when we get this as input again.
65 // Also see documentation there.
66 // https://bugs.kde.org/show_bug.cgi?id=204423
67 u.setHost(QString());
68 QUrlQuery q;
69 q.addQueryItem("kio-workgroup", udsName());
70 u.setQuery(q);
71 }
72 return u.url();
73 }
74 };
75
SMBCDiscovery(const UDSEntry & entry)76 SMBCDiscovery::SMBCDiscovery(const UDSEntry &entry)
77 : m_entry(entry)
78 // cache the name, it may get accessed more than once
79 , m_name(entry.stringValue(KIO::UDSEntry::UDS_NAME))
80 {
81 }
82
udsName() const83 QString SMBCDiscovery::udsName() const
84 {
85 return m_name;
86 }
87
toEntry() const88 KIO::UDSEntry SMBCDiscovery::toEntry() const
89 {
90 return m_entry;
91 }
92
SMBCDiscoverer(const SMBUrl & url,QEventLoop * loop,SMBSlave * slave)93 SMBCDiscoverer::SMBCDiscoverer(const SMBUrl &url, QEventLoop *loop, SMBSlave *slave)
94 : m_url(url)
95 , m_loop(loop)
96 , m_slave(slave)
97 {
98 }
99
~SMBCDiscoverer()100 SMBCDiscoverer::~SMBCDiscoverer()
101 {
102 if (m_dirFd > 0) {
103 smbc_closedir(m_dirFd);
104 }
105 }
106
start()107 void SMBCDiscoverer::start()
108 {
109 queue();
110 }
111
112
discoverNextFileInfo()113 bool SMBCDiscoverer::discoverNextFileInfo()
114 {
115 #ifdef HAVE_READDIRPLUS2
116 // Readdirplus2 dir/file listing. Becomes noop when at end of data associated with dirfd.
117 // If readdirplus2 isn't available the regular dirent listing is done.
118 // readdirplus2 improves performance by giving us a stat without separate call (Samba>=4.12)
119 struct stat st;
120 const struct libsmb_file_info *fileInfo = smbc_readdirplus2(m_dirFd, &st);
121 if (fileInfo) {
122 const QString name = QString::fromUtf8(fileInfo->name);
123 qCDebug(KIO_SMB_LOG) << "fileInfo" << "name:" << name;
124 if (name == ".") {
125 return true;
126 } else if (name == "..") {
127 m_dirWasRoot = false;
128 return true;
129 }
130 UDSEntry entry;
131 entry.reserve(5); // Minimal size. stat will set at least 4 fields.
132 entry.fastInsert(KIO::UDSEntry::UDS_NAME, name);
133
134 m_url.addPath(name);
135 m_slave->statToUDSEntry(m_url, st, entry); // won't produce useful error
136 Q_EMIT newDiscovery(Discovery::Ptr(new SMBCDiscovery(entry)));
137 m_url.cdUp();
138 return true;
139 }
140 #endif // HAVE_READDIRPLUS2
141 return false;
142 }
143
discoverNext()144 void SMBCDiscoverer::discoverNext()
145 {
146 // Poor man's concurrency. smbc isn't thread safe so we'd hold up other
147 // discoverers until we are done. While that will likely happen anyway
148 // because smbc_opendir (usually?) blocks until it actually has all
149 // the data to loop on, meaning the actual looping after open is fairly
150 // fast. Even so, there's benefit in letting other discoverers do
151 // their work in the meantime because they may do more atomic
152 // requests that are async and can take a while due to network latency.
153 // To get somewhat reasonable behavior we simulate an async smbc discovery
154 // by posting loop events to the eventloop and each loop run we process
155 // a single dirent.
156 // This effectively unblocks the eventloop between iterations.
157 // Once we are out of entries this discoverer is considered finished.
158
159 // Always queue a new iteration when returning so we don't forget to.
160 auto autoQueue = qScopeGuard([this] {
161 queue();
162 });
163
164 if (m_dirFd == -1) {
165 init();
166 Q_ASSERT(m_dirFd || m_finished);
167 return;
168 }
169
170 if (discoverNextFileInfo()) {
171 return;
172 }
173
174 qCDebug(KIO_SMB_LOG) << "smbc_readdir ";
175 struct smbc_dirent *dirp = smbc_readdir(m_dirFd);
176 if (dirp == nullptr) {
177 qCDebug(KIO_SMB_LOG) << "done with smbc";
178 stop();
179 return;
180 }
181
182 const QString name = QString::fromUtf8(dirp->name);
183 // We cannot trust dirp->commentlen has it might be with or without the NUL character
184 // See KDE bug #111430 and Samba bug #3030
185 const QString comment = QString::fromUtf8(dirp->comment);
186
187 qCDebug(KIO_SMB_LOG) << "dirent "
188 << "name:" << name
189 << "comment:" << comment
190 << "type:" << dirp->smbc_type;
191
192 UDSEntry entry;
193 // Minimal potential size. The actual size depends on this function,
194 // possibly the stat function, and lastly the Discovery objects themselves.
195 // The smallest will be a ShareDiscovery with 5 fields.
196 entry.reserve(5);
197 entry.fastInsert(KIO::UDSEntry::UDS_NAME, name);
198 entry.fastInsert(KIO::UDSEntry::UDS_COMMENT, comment);
199 // Ensure system shares are marked hidden.
200 if (name.endsWith(QLatin1Char('$'))) {
201 entry.fastInsert(KIO::UDSEntry::UDS_HIDDEN, 1);
202 }
203
204 #if !defined(HAVE_READDIRPLUS2)
205 // . and .. are always of the dir type so they are of no consequence outside
206 // actual dir listing and that'd be done by readdirplus2 already
207 if (name == ".") {
208 // Skip the "." entry
209 // Mind the way m_currentUrl is handled in the loop
210 } else if (name == "..") {
211 m_dirWasRoot = false;
212 } else if (dirp->smbc_type == SMBC_FILE || dirp->smbc_type == SMBC_DIR) {
213 // Set stat information
214 m_url.addPath(name);
215 const int statErr = m_slave->browse_stat_path(m_url, entry);
216 if (statErr != 0) {
217 // The entry can disappear in the time span between
218 // listing and the stat call. There's nothing we or the user
219 // can do about it. Log the incident and move on with listing.
220 qCWarning(KIO_SMB_LOG) << "Failed to stat" << m_url << statErr;
221 } else {
222 Q_EMIT newDiscovery(Discovery::Ptr(new SMBCDiscovery(entry)));
223 }
224 m_url.cdUp();
225 }
226 #endif // HAVE_READDIRPLUS2
227
228 if (dirp->smbc_type == SMBC_SERVER) {
229 Q_EMIT newDiscovery(Discovery::Ptr(new SMBCServerDiscovery(entry)));
230 } else if (dirp->smbc_type == SMBC_FILE_SHARE) {
231 Q_EMIT newDiscovery(Discovery::Ptr(new SMBCShareDiscovery(entry)));
232 } else if (dirp->smbc_type == SMBC_WORKGROUP) {
233 Q_EMIT newDiscovery(Discovery::Ptr(new SMBCWorkgroupDiscovery(entry)));
234 } else {
235 qCDebug(KIO_SMB_LOG) << "SMBC_UNKNOWN :" << name;
236 }
237 }
238
customEvent(QEvent * event)239 void SMBCDiscoverer::customEvent(QEvent *event)
240 {
241 if (event->type() == LoopEvent) {
242 if (!m_finished) {
243 discoverNext();
244 }
245 return;
246 }
247 QObject::customEvent(event);
248 }
249
stop()250 void SMBCDiscoverer::stop()
251 {
252 m_finished = true;
253 Q_EMIT finished();
254 }
255
isFinished() const256 bool SMBCDiscoverer::isFinished() const
257 {
258 return m_finished;
259 }
260
dirWasRoot() const261 bool SMBCDiscoverer::dirWasRoot() const
262 {
263 return m_dirWasRoot;
264 }
265
error() const266 int SMBCDiscoverer::error() const
267 {
268 return m_error;
269 }
270
init()271 void SMBCDiscoverer::init()
272 {
273 Q_ASSERT(m_dirFd < 0);
274
275 m_dirFd = smbc_opendir(m_url.toSmbcUrl());
276 if (m_dirFd >= 0) {
277 m_error = 0;
278 } else {
279 m_error = errno;
280 stop();
281 }
282
283 qCDebug(KIO_SMB_LOG) << "open" << m_url.toSmbcUrl()
284 << "url-type:" << m_url.getType()
285 << "dirfd:" << m_dirFd
286 << "errNum:" << m_error;
287
288 return;
289 }
290
queue()291 void SMBCDiscoverer::queue()
292 {
293 if (m_finished) {
294 return;
295 }
296
297 // Queue low priority events. For server discovery (that is: other discoverers run as well)
298 // we want the modern discoverers to be peferred. For other discoveries only
299 // SMBC is running on the loop and so the priority has no negative impact.
300 QCoreApplication::postEvent(this, new QEvent(LoopEvent), Qt::LowEventPriority);
301 }
302