xref: /illumos-gate/usr/src/uts/common/fs/smbsrv/smb_mangle_name.c (revision 66582b606a8194f7f3ba5b3a3a6dca5b0d346361)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9  * or http://www.opensolaris.org/os/licensing.
10  * See the License for the specific language governing permissions
11  * and limitations under the License.
12  *
13  * When distributing Covered Code, include this CDDL HEADER in each
14  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15  * If applicable, add the following below this CDDL HEADER, with the
16  * fields enclosed by brackets "[]" replaced with your own identifying
17  * information: Portions Copyright [yyyy] [name of copyright owner]
18  *
19  * CDDL HEADER END
20  */
21 /*
22  * Copyright (c) 2007, 2010, Oracle and/or its affiliates. All rights reserved.
23  * Copyright 2017 Nexenta Systems, Inc.  All rights reserved.
24  */
25 
26 #include <sys/types.h>
27 #include <sys/param.h>
28 #include <sys/sunddi.h>
29 #include <sys/errno.h>
30 #include <sys/extdirent.h>
31 #include <smbsrv/string.h>
32 #include <smbsrv/smb_vops.h>
33 #include <smbsrv/smb_kproto.h>
34 #include <smbsrv/smb_fsops.h>
35 
36 /*
37  * Characters we don't allow in DOS file names.
38  * If a filename contains any of these chars, it should get mangled.
39  *
40  * '.' is also an invalid DOS char but since it's a special
41  * case it doesn't appear in the list.
42  */
43 static const char invalid_dos_chars[] =
44 	"\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017"
45 	"\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037"
46 	" \"/\\:|<>*?";
47 
48 /*
49  * According to MSKB article #142982, Windows deletes invalid chars and
50  * spaces from file name in mangling process; and invalid chars include:
51  * ."/\[]:;=,
52  *
53  * But some of these chars and some other chars (e.g. +) are replaced
54  * with underscore (_). They are introduced here as special chars.
55  */
56 static const char special_chars[] = "[];=,+";
57 
58 #define	isinvalid(c)	(strchr(invalid_dos_chars, c) || (c & 0x80))
59 
60 static int smb_generate_mangle(uint64_t, char *, size_t);
61 static char smb_mangle_char(char);
62 
63 /*
64  * Return true if name contains characters that are invalid in a file
65  * name or it is a reserved DOS device name.  Otherwise, returns false.
66  *
67  * Control characters (values 0 - 31) and the following characters are
68  * invalid:
69  *	< > : " / \ | ? *
70  */
71 boolean_t
72 smb_is_invalid_filename(const char *name)
73 {
74 	const char *p;
75 
76 	if ((p = strpbrk(name, invalid_dos_chars)) != NULL) {
77 		if (*p != ' ')
78 			return (B_TRUE);
79 	}
80 
81 	return (smb_is_reserved_dos_name(name));
82 }
83 
84 /*
85  * smb_is_reserved_dos_name
86  *
87  * This function checks if the name is a reserved DOS device name.
88  * The device name should not be followed immediately by an extension,
89  * for example, NUL.txt.
90  */
91 boolean_t
92 smb_is_reserved_dos_name(const char *name)
93 {
94 	static char *cnames[] = { "CLOCK$", "COM1", "COM2", "COM3", "COM4",
95 		"COM5", "COM6", "COM7", "COM8", "COM9", "CON" };
96 	static char *lnames[] = { "LPT1", "LPT2", "LPT3", "LPT4", "LPT5",
97 		"LPT6", "LPT7", "LPT8", "LPT9" };
98 	static char *others[] = { "AUX", "NUL", "PRN" };
99 	char	**reserved;
100 	char	ch;
101 	int	n_reserved;
102 	int	len;
103 	int	i;
104 
105 	ch = smb_toupper(*name);
106 
107 	switch (ch) {
108 	case 'A':
109 	case 'N':
110 	case 'P':
111 		reserved = others;
112 		n_reserved = sizeof (others) / sizeof (others[0]);
113 		break;
114 	case 'C':
115 		reserved = cnames;
116 		n_reserved = sizeof (cnames) / sizeof (cnames[0]);
117 		break;
118 	case 'L':
119 		reserved = lnames;
120 		n_reserved = sizeof (lnames) / sizeof (lnames[0]);
121 		break;
122 	default:
123 		return (B_FALSE);
124 	}
125 
126 	for (i  = 0; i < n_reserved; ++i) {
127 		len = strlen(reserved[i]);
128 
129 		if (smb_strcasecmp(reserved[i], name, len) == 0) {
130 			ch = *(name + len);
131 			if ((ch == '\0') || (ch == '.'))
132 				return (B_TRUE);
133 		}
134 	}
135 
136 	return (B_FALSE);
137 }
138 
139 /*
140  * smb_needs_mangled
141  *
142  * A name needs to be mangled if any of the following are true:
143  * - the first character is dot (.) and the name is not "." or ".."
144  * - the name contains illegal or special charsacter
145  * - the name name length > 12
146  * - the number of dots == 0 and length > 8
147  * - the number of dots > 1
148  * - the number of dots == 1 and name is not 8.3
149  */
150 boolean_t
151 smb_needs_mangled(const char *name)
152 {
153 	int len, extlen, ndots;
154 	const char *p;
155 	const char *last_dot;
156 
157 	if ((strcmp(name, ".") == 0) || (strcmp(name, "..") == 0))
158 		return (B_FALSE);
159 
160 	if (*name == '.')
161 		return (B_TRUE);
162 
163 	len = 0;
164 	ndots = 0;
165 	last_dot = NULL;
166 	for (p = name; *p != '\0'; ++p) {
167 		if (smb_iscntrl(*p) ||
168 		    (strchr(special_chars, *p) != NULL) ||
169 		    (strchr(invalid_dos_chars, *p)) != NULL)
170 			return (B_TRUE);
171 
172 		if (*p == '.') {
173 			++ndots;
174 			last_dot = p;
175 		}
176 		++len;
177 	}
178 
179 	if ((len > SMB_NAME83_LEN) ||
180 	    (ndots == 0 && len > SMB_NAME83_BASELEN) ||
181 	    (ndots > 1)) {
182 		return (B_TRUE);
183 	}
184 
185 	if (last_dot != NULL) {
186 		extlen = strlen(last_dot + 1);
187 		if ((extlen == 0) || (extlen > SMB_NAME83_EXTLEN))
188 			return (B_TRUE);
189 
190 		if ((len - extlen - 1) > SMB_NAME83_BASELEN)
191 			return (B_TRUE);
192 	}
193 
194 	return (B_FALSE);
195 }
196 
197 /*
198  * smb_mangle_char
199  *
200  * If c is an invalid DOS character or non-ascii, it should
201  * not be used in the mangled name. We return -1 to indicate
202  * an invalid character.
203  *
204  * If c is a special chars, it should be replaced with '_'.
205  *
206  * Otherwise c is returned as uppercase.
207  */
208 static char
209 smb_mangle_char(char c)
210 {
211 	if (isinvalid(c))
212 		return (-1);
213 
214 	if (strchr(special_chars, c))
215 		return ('_');
216 
217 	return (smb_toupper(c));
218 }
219 
220 /*
221  * smb_generate_mangle
222  *
223  * Generate a mangle string containing at least 2 characters and
224  * at most (buflen - 1) characters.
225  *
226  * Returns the number of chars in the generated mangle.
227  */
228 static int
229 smb_generate_mangle(uint64_t fid, char *buf, size_t buflen)
230 {
231 	static char *base36 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
232 	char *p = buf;
233 	int i;
234 
235 	if (fid == 0)
236 		fid = (uint64_t)-1;
237 
238 	*p++ = '~';
239 	for (i = 2; (i < buflen) && (fid > 0); fid /= 36, ++i)
240 		*p++ = base36[fid % 36];
241 	*p = '\0';
242 
243 	return (i - 1);
244 }
245 
246 /*
247  * smb_maybe_mangled
248  *
249  * Mangled names should be valid DOS file names: less than 12 characters
250  * long, contain at least one tilde character and conform to an 8.3 name
251  * format.
252  *
253  * Returns true if the name looks like a mangled name.
254  */
255 boolean_t
256 smb_maybe_mangled(char *name)
257 {
258 	const char *p;
259 	boolean_t has_tilde = B_FALSE;
260 	int ndots = 0;
261 	int i;
262 
263 	for (p = name, i = 0; (*p != '\0') && (i < SMB_NAME83_LEN); i++, p++) {
264 		if ((strchr(special_chars, *p) != NULL) ||
265 		    (strchr(invalid_dos_chars, *p) != NULL))
266 			return (B_FALSE);
267 
268 		if (*p == '.') {
269 			if ((++ndots) > 1)
270 				return (B_FALSE);
271 		}
272 
273 		if ((*p == '~') && (i < SMB_NAME83_BASELEN))
274 			has_tilde = B_TRUE;
275 
276 		if (*p == '.' && !has_tilde)
277 			return (B_FALSE);
278 	}
279 
280 	return ((*p == '\0') && has_tilde);
281 }
282 
283 /*
284  * smb_mangle
285  *
286  * Microsoft knowledge base article #142982 describes how Windows
287  * generates 8.3 filenames from long file names. Some other details
288  * can be found in article #114816.
289  *
290  * This function will mangle the name whether mangling is required
291  * or not. Callers should use smb_needs_mangled() to determine whether
292  * mangling is required.
293  *
294  * name		original file name
295  * fid		inode number to generate unique mangle
296  * buf		output buffer (buflen bytes) to contain mangled name
297  */
298 void
299 smb_mangle(const char *name, ino64_t fid, char *buf, size_t buflen)
300 {
301 	int i, avail;
302 	const char *p;
303 	char c;
304 	char *pbuf;
305 	char mangle_buf[SMB_NAME83_BASELEN];
306 
307 	ASSERT(name && buf && (buflen >= SMB_SHORTNAMELEN));
308 
309 	avail = SMB_NAME83_BASELEN -
310 	    smb_generate_mangle(fid, mangle_buf, SMB_NAME83_BASELEN);
311 	name += strspn(name, ".");
312 
313 	/*
314 	 * Copy up to avail characters from the base part of name
315 	 * to buf then append the generated mangle string.
316 	 */
317 	p = name;
318 	pbuf = buf;
319 	for (i = 0; (i < avail) && (*p != '\0') && (*p != '.'); ++i, ++p) {
320 		if ((c = smb_mangle_char(*p)) == -1)
321 			continue;
322 		*pbuf++ = c;
323 	}
324 	*pbuf = '\0';
325 	(void) strlcat(pbuf, mangle_buf, SMB_NAME83_BASELEN);
326 	pbuf = strchr(pbuf, '\0');
327 
328 	/*
329 	 * Find the last dot in the name. If there is a dot and an
330 	 * extension, append '.' and up to SMB_NAME83_EXTLEN extension
331 	 * characters to the mangled name.
332 	 */
333 	if (((p = strrchr(name, '.')) != NULL) && (*(++p) != '\0')) {
334 		*pbuf++ = '.';
335 		for (i = 0; (i < SMB_NAME83_EXTLEN) && (*p != '\0'); ++i, ++p) {
336 			if ((c = smb_mangle_char(*p)) == -1)
337 				continue;
338 			*pbuf++ = c;
339 		}
340 	}
341 
342 	*pbuf = '\0';
343 }
344 
345 /*
346  * smb_unmangle
347  *
348  * Given a mangled name, try to find the real file name as it appears
349  * in the directory entry.
350  *
351  * smb_unmangle should only be called on names for which
352  * smb_maybe_mangled() is true
353  *
354  * The flags arg is no longer used, but retained just to avoid
355  * changing the many callers of this function.
356  *
357  * Returns:
358  *   0       - SUCCESS. Unmangled name is returned in namebuf.
359  *   EINVAL  - a parameter was invalid.
360  *   ENOTDIR - dnode is not a directory node.
361  *   ENOENT  - an unmangled name could not be found.
362  */
363 #define	SMB_UNMANGLE_BUFSIZE	(4 * 1024)
364 int
365 smb_unmangle(smb_node_t *dnode, char *name, char *namebuf,
366     int buflen, uint32_t flags)
367 {
368 	_NOTE(ARGUNUSED(flags))	// avoid changing all callers
369 	int		err, eof, bufsize;
370 	uint64_t	offset;
371 	ino64_t		ino;
372 	char		*namep, *buf;
373 	char		shortname[SMB_SHORTNAMELEN];
374 	vnode_t		*vp;
375 	char		*bufptr;
376 	dirent64_t	*dp;
377 	cred_t		*cr = zone_kcred();
378 	int		rc = ENOENT;
379 
380 	if (dnode == NULL || name == NULL || namebuf == NULL || buflen == 0)
381 		return (EINVAL);
382 
383 	ASSERT(smb_maybe_mangled(name) == B_TRUE);
384 
385 	if (!smb_node_is_dir(dnode))
386 		return (ENOTDIR);
387 
388 	vp = dnode->vp;
389 	*namebuf = '\0';
390 
391 	buf = kmem_alloc(SMB_UNMANGLE_BUFSIZE, KM_SLEEP);
392 	bufptr = buf;
393 	bufsize = 0;
394 	offset = 0;	// next entry offset
395 	eof = B_FALSE;
396 
397 	for (;;) {
398 		/*
399 		 * Read some entries, if buffer empty or
400 		 * we've scanned all of it.  Flags zero
401 		 * (no edirent, no ABE wanted here)
402 		 */
403 		if (bufsize <= 0) {
404 			bufsize = SMB_UNMANGLE_BUFSIZE;
405 			rc = smb_vop_readdir(vp, offset, buf,
406 			    &bufsize, &eof, 0, cr);
407 			if (rc != 0)
408 				break; /* error */
409 			if (bufsize == 0) {
410 				eof = B_TRUE;
411 				rc = ENOENT;
412 				break;
413 			}
414 			bufptr = buf;
415 		}
416 		/* LINTED pointer alignment */
417 		dp = (dirent64_t *)bufptr;
418 
419 		/*
420 		 * Partial records are not supposed to happen,
421 		 * but let's be defensive. If this happens,
422 		 * restart at the current offset.
423 		 */
424 		bufptr += dp->d_reclen;
425 		bufsize -= dp->d_reclen;
426 		if (bufsize < 0)
427 			continue;
428 
429 		offset = dp->d_off;
430 		ino = dp->d_ino;
431 		namep = dp->d_name;
432 
433 		/* skip non utf8 filename */
434 		if (u8_validate(namep, strlen(namep), NULL,
435 		    U8_VALIDATE_ENTIRE, &err) < 0)
436 			continue;
437 
438 		smb_mangle(namep, ino, shortname, SMB_SHORTNAMELEN);
439 		if (smb_strcasecmp(name, shortname, 0) == 0) {
440 			(void) strlcpy(namebuf, namep, buflen);
441 			rc = 0;
442 			break;
443 		}
444 	}
445 
446 	kmem_free(buf, SMB_UNMANGLE_BUFSIZE);
447 	return (rc);
448 }
449