]>
Commit | Line | Data |
---|---|---|
23c9e63c DD |
1 | /* |
2 | * Copyright (c) 2000, Red Hat, Inc. | |
3 | * | |
4 | * This program is free software; you can redistribute it and/or modify | |
5 | * it under the terms of the GNU General Public License as published by | |
6 | * the Free Software Foundation; either version 2 of the License, or | |
7 | * (at your option) any later version. | |
8 | * | |
9 | * A copy of the GNU General Public License can be found at | |
10 | * http://www.gnu.org/ | |
11 | * | |
12 | * Written by DJ Delorie <dj@cygnus.com> | |
13 | * | |
14 | */ | |
15 | ||
16 | /* The purpose of this file is to hide all the details about accessing | |
17 | Cygwin's mount table. If the format or location of the mount table | |
18 | changes, this is the file to change to match it. */ | |
19 | ||
b24c88b3 | 20 | #if 0 |
3c054baf | 21 | static const char *cvsid = "\n%%% $Id$\n"; |
b24c88b3 | 22 | #endif |
8507f105 | 23 | |
23c9e63c DD |
24 | #include "win32.h" |
25 | ||
26 | #include <stdio.h> | |
a351e48c | 27 | #include <stdlib.h> |
db04fc41 RC |
28 | |
29 | // These headers aren't available outside the winsup tree | |
30 | // #include "../cygwin/include/cygwin/version.h" | |
31 | // KEEP SYNCHRONISED WITH /src/winsup/cygwin/include/cygwin/version.h | |
32 | ||
33 | #define CYGWIN_INFO_CYGNUS_REGISTRY_NAME "Cygnus Solutions" | |
34 | #define CYGWIN_INFO_CYGWIN_REGISTRY_NAME "Cygwin" | |
35 | #define CYGWIN_INFO_CYGWIN_MOUNT_REGISTRY_NAME "mounts v2" | |
36 | #define CYGWIN_INFO_CYGDRIVE_FLAGS "cygdrive flags" | |
37 | #define CYGWIN_INFO_CYGDRIVE_PREFIX "cygdrive prefix" | |
38 | #define CYGWIN_INFO_CYGDRIVE_DEFAULT_PREFIX "/cygdrive" | |
39 | ||
40 | ||
41 | // #include "../cygwin/include/sys/mount.h" | |
42 | ||
43 | // KEEP SYNCHRONISED WITH /src/winsup/cygwin/include/sys/mount.h | |
44 | #ifdef __cplusplus | |
45 | extern "C" { | |
46 | #endif | |
47 | ||
48 | enum | |
49 | { | |
50 | MOUNT_SYMLINK = 0x001, /* "mount point" is a symlink */ | |
51 | MOUNT_BINARY = 0x002, /* "binary" format read/writes */ | |
52 | MOUNT_SYSTEM = 0x008, /* mount point came from system table */ | |
53 | MOUNT_EXEC = 0x010, /* Any file in the mounted directory gets 'x' bit */ | |
54 | MOUNT_AUTO = 0x020, /* mount point refers to auto device mount */ | |
55 | MOUNT_CYGWIN_EXEC = 0x040, /* file or directory is or contains a cygwin executable */ | |
56 | MOUNT_MIXED = 0x080, /* reads are text, writes are binary */ | |
57 | }; | |
58 | ||
59 | // int mount (const char *, const char *, unsigned __flags); | |
60 | // int umount (const char *); | |
61 | // int cygwin_umount (const char *__path, unsigned __flags); | |
62 | ||
63 | #ifdef __cplusplus | |
64 | }; | |
65 | #endif | |
66 | ||
67 | ||
23c9e63c DD |
68 | |
69 | #include "mount.h" | |
70 | #include "msg.h" | |
71 | #include "resource.h" | |
72 | #include "dialog.h" | |
a351e48c | 73 | #include "state.h" |
23c9e63c | 74 | |
3c054baf RC |
75 | #include "String++.h" |
76 | ||
1ac649ed RC |
77 | /* Used when treating / and \ as equivalent. */ |
78 | #define SLASH_P(ch) \ | |
79 | ({ \ | |
80 | char __c = (ch); \ | |
81 | ((__c) == '/' || (__c) == '\\'); \ | |
82 | }) | |
83 | ||
a351e48c | 84 | static struct mnt |
b24c88b3 | 85 | { |
3c054baf RC |
86 | String native; |
87 | String posix; | |
b24c88b3 RC |
88 | int istext; |
89 | } | |
90 | mount_table[255]; | |
23c9e63c | 91 | |
85b43844 CF |
92 | struct mnt *root_here = NULL; |
93 | ||
3c054baf RC |
94 | static String |
95 | find2 (HKEY rkey, int *istext, String const &what) | |
23c9e63c | 96 | { |
a351e48c | 97 | HKEY key; |
23c9e63c | 98 | |
3c054baf RC |
99 | if (RegOpenKeyEx (rkey, what.cstr_oneuse (), 0, KEY_READ, &key) != |
100 | ERROR_SUCCESS) | |
23c9e63c DD |
101 | return 0; |
102 | ||
3c054baf RC |
103 | DWORD retvallen = 0; |
104 | DWORD type; | |
105 | ||
106 | String Sretval; | |
23c9e63c DD |
107 | if (RegQueryValueEx (key, "native", 0, &type, 0, &retvallen) |
108 | == ERROR_SUCCESS) | |
109 | { | |
3c054baf | 110 | char retval[retvallen]; |
b24c88b3 RC |
111 | if (RegQueryValueEx |
112 | (key, "native", 0, &type, (BYTE *) retval, | |
3c054baf RC |
113 | &retvallen) == ERROR_SUCCESS) |
114 | Sretval = String (retval); | |
23c9e63c DD |
115 | } |
116 | ||
3c054baf | 117 | DWORD flags = 0; |
23c9e63c | 118 | retvallen = sizeof (flags); |
b24c88b3 | 119 | RegQueryValueEx (key, "flags", 0, &type, (BYTE *) & flags, &retvallen); |
23c9e63c DD |
120 | |
121 | RegCloseKey (key); | |
122 | ||
3c054baf | 123 | if (Sretval.size ()) |
23c9e63c | 124 | *istext = (flags & MOUNT_BINARY) ? 0 : 1; |
23c9e63c | 125 | |
3c054baf | 126 | return Sretval; |
23c9e63c DD |
127 | } |
128 | ||
129 | void | |
3c054baf RC |
130 | create_mount (String const posix, String const win32, int istext, |
131 | int issystem) | |
23c9e63c DD |
132 | { |
133 | char buf[1000]; | |
23c9e63c | 134 | HKEY key; |
b24c88b3 | 135 | DWORD disposition; |
23c9e63c DD |
136 | DWORD flags; |
137 | ||
138 | remove_mount (posix); | |
139 | ||
140 | sprintf (buf, "Software\\%s\\%s\\%s\\%s", | |
141 | CYGWIN_INFO_CYGNUS_REGISTRY_NAME, | |
142 | CYGWIN_INFO_CYGWIN_REGISTRY_NAME, | |
3c054baf | 143 | CYGWIN_INFO_CYGWIN_MOUNT_REGISTRY_NAME, posix.cstr_oneuse ()); |
23c9e63c | 144 | |
24e259bb | 145 | HKEY kr = issystem ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER; |
c90bc3df | 146 | if (RegCreateKeyEx (kr, buf, 0, (char *)"Cygwin", 0, KEY_ALL_ACCESS, |
23c9e63c DD |
147 | 0, &key, &disposition) != ERROR_SUCCESS) |
148 | fatal ("mount"); | |
149 | ||
3c054baf RC |
150 | RegSetValueEx (key, "native", 0, REG_SZ, (BYTE *) win32.cstr_oneuse (), |
151 | win32.size () + 1); | |
24e259bb DD |
152 | flags = 0; |
153 | if (!istext) | |
154 | flags |= MOUNT_BINARY; | |
155 | if (issystem) | |
156 | flags |= MOUNT_SYSTEM; | |
b24c88b3 RC |
157 | RegSetValueEx (key, "flags", 0, REG_DWORD, (BYTE *) & flags, |
158 | sizeof (flags)); | |
159 | ||
160 | RegCloseKey (key); | |
0af2d779 | 161 | read_mounts (); |
23c9e63c DD |
162 | } |
163 | ||
164 | static void | |
3c054baf | 165 | remove1 (HKEY rkey, String const posix) |
23c9e63c DD |
166 | { |
167 | char buf[1000]; | |
168 | ||
169 | sprintf (buf, "Software\\%s\\%s\\%s\\%s", | |
170 | CYGWIN_INFO_CYGNUS_REGISTRY_NAME, | |
171 | CYGWIN_INFO_CYGWIN_REGISTRY_NAME, | |
3c054baf | 172 | CYGWIN_INFO_CYGWIN_MOUNT_REGISTRY_NAME, posix.cstr_oneuse ()); |
23c9e63c DD |
173 | |
174 | RegDeleteKey (rkey, buf); | |
175 | } | |
176 | ||
177 | void | |
3c054baf | 178 | remove_mount (String const posix) |
23c9e63c DD |
179 | { |
180 | remove1 (HKEY_LOCAL_MACHINE, posix); | |
181 | remove1 (HKEY_CURRENT_USER, posix); | |
182 | } | |
f8a6415f DD |
183 | |
184 | static void | |
185 | set_cygdrive_flags (HKEY key, int istext, DWORD cygdrive_flags) | |
186 | { | |
187 | int cur_istext = (cygdrive_flags & MOUNT_BINARY) ? 0 : 1; | |
188 | if (cur_istext != istext) | |
189 | { | |
190 | if (!istext) | |
191 | cygdrive_flags |= MOUNT_BINARY; | |
192 | else | |
193 | cygdrive_flags &= ~MOUNT_BINARY; | |
194 | RegSetValueEx (key, CYGWIN_INFO_CYGDRIVE_FLAGS, 0, REG_DWORD, | |
b24c88b3 | 195 | (BYTE *) & cygdrive_flags, sizeof (cygdrive_flags)); |
f8a6415f DD |
196 | } |
197 | } | |
198 | ||
199 | static LONG | |
b24c88b3 | 200 | get_cygdrive_flags (HKEY key, DWORD * cygdrive_flags) |
f8a6415f | 201 | { |
b24c88b3 | 202 | DWORD retvallen = sizeof (*cygdrive_flags); |
f8a6415f | 203 | LONG status = RegQueryValueEx (key, CYGWIN_INFO_CYGDRIVE_FLAGS, 0, 0, |
b24c88b3 | 204 | (BYTE *) cygdrive_flags, &retvallen); |
f8a6415f DD |
205 | return status; |
206 | } | |
207 | ||
208 | static DWORD | |
b24c88b3 | 209 | default_cygdrive (HKEY key) |
f8a6415f DD |
210 | { |
211 | RegSetValueEx (key, CYGWIN_INFO_CYGDRIVE_PREFIX, 0, REG_SZ, | |
b24c88b3 | 212 | (BYTE *) CYGWIN_INFO_CYGDRIVE_DEFAULT_PREFIX, |
f8a6415f DD |
213 | strlen (CYGWIN_INFO_CYGDRIVE_DEFAULT_PREFIX) + 1); |
214 | DWORD cygdrive_flags = MOUNT_AUTO; | |
215 | RegSetValueEx (key, CYGWIN_INFO_CYGDRIVE_FLAGS, 0, REG_DWORD, | |
b24c88b3 | 216 | (BYTE *) & cygdrive_flags, sizeof (cygdrive_flags)); |
f8a6415f DD |
217 | return cygdrive_flags; |
218 | } | |
219 | ||
220 | void | |
221 | set_cygdrive_flags (int istext, int issystem) | |
222 | { | |
223 | int found_system = 0; | |
224 | ||
225 | char buf[1000]; | |
226 | sprintf (buf, "Software\\%s\\%s\\%s", | |
227 | CYGWIN_INFO_CYGNUS_REGISTRY_NAME, | |
228 | CYGWIN_INFO_CYGWIN_REGISTRY_NAME, | |
229 | CYGWIN_INFO_CYGWIN_MOUNT_REGISTRY_NAME); | |
230 | ||
231 | if (issystem) | |
232 | { | |
233 | HKEY key; | |
234 | DWORD disposition; | |
235 | LONG status = RegCreateKeyEx (HKEY_LOCAL_MACHINE, buf, 0, 0, 0, | |
b24c88b3 | 236 | KEY_ALL_ACCESS, 0, &key, &disposition); |
f8a6415f DD |
237 | if (status == ERROR_SUCCESS) |
238 | { | |
239 | DWORD cygdrive_flags = 0; | |
240 | status = get_cygdrive_flags (key, &cygdrive_flags); | |
241 | if (status == ERROR_SUCCESS) | |
242 | { | |
243 | set_cygdrive_flags (key, istext, cygdrive_flags); | |
244 | found_system = 1; | |
245 | } | |
b24c88b3 | 246 | RegCloseKey (key); |
f8a6415f DD |
247 | } |
248 | } | |
249 | ||
250 | HKEY key; | |
251 | DWORD disposition; | |
b24c88b3 RC |
252 | LONG status = |
253 | RegCreateKeyEx (HKEY_CURRENT_USER, buf, 0, 0, 0, KEY_ALL_ACCESS, | |
254 | 0, &key, &disposition); | |
f8a6415f DD |
255 | if (status != ERROR_SUCCESS) |
256 | fatal ("set_cygdrive_flags"); | |
257 | ||
258 | DWORD cygdrive_flags = 0; | |
259 | status = get_cygdrive_flags (key, &cygdrive_flags); | |
260 | if (status == ERROR_FILE_NOT_FOUND && !found_system) | |
261 | { | |
b24c88b3 | 262 | cygdrive_flags = default_cygdrive (key); |
f8a6415f DD |
263 | status = ERROR_SUCCESS; |
264 | } | |
265 | ||
266 | if (status == ERROR_SUCCESS) | |
267 | set_cygdrive_flags (key, istext, cygdrive_flags); | |
268 | ||
b24c88b3 | 269 | RegCloseKey (key); |
f8a6415f | 270 | } |
a351e48c | 271 | |
85b43844 | 272 | static int |
a351e48c CF |
273 | in_table (struct mnt *m) |
274 | { | |
b24c88b3 | 275 | for (struct mnt * m1 = mount_table; m1 < m; m1++) |
3c054baf | 276 | if (m1->posix.casecompare (m->posix) == 0) |
a351e48c CF |
277 | return 1; |
278 | return 0; | |
279 | } | |
280 | ||
281 | /* | |
282 | * is_admin () determines whether or not the current user is a member of the | |
283 | * Administrators group. On Windows 9X, the current user is considered an | |
284 | * Administrator by definition. | |
285 | */ | |
286 | ||
287 | static int | |
288 | is_admin () | |
289 | { | |
290 | // Windows 9X users are considered Administrators by definition | |
291 | OSVERSIONINFO verinfo; | |
292 | verinfo.dwOSVersionInfoSize = sizeof (verinfo); | |
293 | GetVersionEx (&verinfo); | |
294 | if (verinfo.dwPlatformId != VER_PLATFORM_WIN32_NT) | |
295 | return 1; | |
296 | ||
297 | // Get the process token for the current process | |
298 | HANDLE token; | |
b24c88b3 | 299 | BOOL status = OpenProcessToken (GetCurrentProcess (), TOKEN_QUERY, &token); |
a351e48c CF |
300 | if (!status) |
301 | return 0; | |
302 | ||
303 | // Get the group token information | |
304 | UCHAR token_info[1024]; | |
305 | PTOKEN_GROUPS groups = (PTOKEN_GROUPS) token_info; | |
306 | DWORD token_info_len = sizeof (token_info); | |
b24c88b3 RC |
307 | status = |
308 | GetTokenInformation (token, TokenGroups, token_info, token_info_len, | |
309 | &token_info_len); | |
310 | CloseHandle (token); | |
a351e48c CF |
311 | if (!status) |
312 | return 0; | |
313 | ||
314 | // Create the Administrators group SID | |
315 | PSID admin_sid; | |
b24c88b3 RC |
316 | SID_IDENTIFIER_AUTHORITY authority = { SECURITY_NT_AUTHORITY }; |
317 | status = | |
318 | AllocateAndInitializeSid (&authority, 2, SECURITY_BUILTIN_DOMAIN_RID, | |
319 | DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, | |
320 | &admin_sid); | |
a351e48c CF |
321 | if (!status) |
322 | return 0; | |
323 | ||
324 | // Check to see if the user is a member of the Administrators group | |
325 | status = 0; | |
b24c88b3 RC |
326 | for (UINT i = 0; i < groups->GroupCount; i++) |
327 | { | |
328 | if (EqualSid (groups->Groups[i].Sid, admin_sid)) | |
329 | { | |
330 | status = 1; | |
331 | break; | |
332 | } | |
a351e48c | 333 | } |
a351e48c CF |
334 | |
335 | // Destroy the Administrators group SID | |
336 | FreeSid (admin_sid); | |
337 | ||
338 | // Return whether or not the user is a member of the Administrators group | |
339 | return status; | |
340 | } | |
341 | ||
342 | void | |
343 | read_mounts () | |
344 | { | |
345 | DWORD posix_path_size; | |
346 | int res; | |
347 | struct mnt *m = mount_table; | |
348 | DWORD disposition; | |
349 | char buf[10000]; | |
350 | ||
0af2d779 | 351 | root_here = NULL; |
3c054baf | 352 | for (mnt * m1 = mount_table; m1->posix.size (); m1++) |
0af2d779 | 353 | { |
3c054baf RC |
354 | m1->posix = String (); //empty string; |
355 | m1->native = String (); | |
0af2d779 CF |
356 | } |
357 | ||
a351e48c CF |
358 | /* Loop through subkeys */ |
359 | /* FIXME: we would like to not check MAX_MOUNTS but the heap in the | |
360 | shared area is currently statically allocated so we can't have an | |
361 | arbitrarily large number of mounts. */ | |
362 | for (int issystem = 0; issystem <= 1; issystem++) | |
363 | { | |
364 | sprintf (buf, "Software\\%s\\%s\\%s", | |
365 | CYGWIN_INFO_CYGNUS_REGISTRY_NAME, | |
366 | CYGWIN_INFO_CYGWIN_REGISTRY_NAME, | |
367 | CYGWIN_INFO_CYGWIN_MOUNT_REGISTRY_NAME); | |
368 | ||
369 | HKEY key = issystem ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER; | |
c90bc3df | 370 | if (RegCreateKeyEx (key, buf, 0, (char *)"Cygwin", 0, KEY_ALL_ACCESS, |
a351e48c CF |
371 | 0, &key, &disposition) != ERROR_SUCCESS) |
372 | break; | |
b24c88b3 | 373 | for (int i = 0;; i++, m++) |
a351e48c | 374 | { |
3c054baf | 375 | char aBuffer[MAX_PATH + 1]; |
a351e48c CF |
376 | posix_path_size = MAX_PATH; |
377 | /* FIXME: if maximum posix_path_size is 256, we're going to | |
378 | run into problems if we ever try to store a mount point that's | |
379 | over 256 but is under MAX_PATH. */ | |
3c054baf | 380 | res = RegEnumKeyEx (key, i, aBuffer, &posix_path_size, NULL, |
a351e48c CF |
381 | NULL, NULL, NULL); |
382 | ||
383 | if (res == ERROR_NO_MORE_ITEMS) | |
0af2d779 | 384 | { |
3c054baf | 385 | m->posix = String (); |
0af2d779 CF |
386 | break; |
387 | } | |
3c054baf | 388 | m->posix = String (aBuffer); |
a351e48c | 389 | |
3c054baf | 390 | if (!m->posix.size () || in_table (m)) |
0af2d779 | 391 | goto no_go; |
a351e48c CF |
392 | else if (res != ERROR_SUCCESS) |
393 | break; | |
394 | else | |
395 | { | |
396 | m->native = find2 (key, &m->istext, m->posix); | |
3c054baf | 397 | if (!m->native.size ()) |
0af2d779 | 398 | goto no_go; |
b24c88b3 | 399 | |
3c054baf | 400 | if (m->posix == "/") |
a351e48c | 401 | { |
85b43844 | 402 | root_here = m; |
a351e48c CF |
403 | if (m->istext) |
404 | root_text = IDC_ROOT_TEXT; | |
405 | else | |
406 | root_text = IDC_ROOT_BINARY; | |
407 | if (issystem) | |
408 | root_scope = IDC_ROOT_SYSTEM; | |
409 | else | |
410 | root_scope = IDC_ROOT_USER; | |
411 | } | |
412 | } | |
0af2d779 CF |
413 | continue; |
414 | no_go: | |
3c054baf RC |
415 | m->posix = String (); |
416 | --m; | |
a351e48c CF |
417 | } |
418 | RegCloseKey (key); | |
419 | } | |
420 | ||
85b43844 | 421 | if (!root_here) |
a351e48c | 422 | { |
85b43844 | 423 | root_here = m; |
3c054baf | 424 | m->posix = String ("/"); |
a351e48c | 425 | char windir[_MAX_PATH]; |
a351e48c | 426 | root_text = IDC_ROOT_BINARY; |
b24c88b3 | 427 | root_scope = (is_admin ())? IDC_ROOT_SYSTEM : IDC_ROOT_USER; |
85b43844 CF |
428 | GetWindowsDirectory (windir, sizeof (windir)); |
429 | windir[2] = 0; | |
3c054baf | 430 | set_root_dir (String (windir) + "\\cygwin"); |
0af2d779 | 431 | m++; |
a351e48c | 432 | } |
a351e48c CF |
433 | } |
434 | ||
85b43844 | 435 | void |
3c054baf | 436 | set_root_dir (String const val) |
85b43844 CF |
437 | { |
438 | root_here->native = val; | |
439 | } | |
440 | ||
3c054baf | 441 | String const |
85b43844 CF |
442 | get_root_dir () |
443 | { | |
3c054baf | 444 | return root_here ? root_here->native : String (); |
85b43844 CF |
445 | } |
446 | ||
a351e48c CF |
447 | /* Return non-zero if PATH1 is a prefix of PATH2. |
448 | Both are assumed to be of the same path style and / vs \ usage. | |
449 | Neither may be "". | |
a351e48c CF |
450 | |
451 | Examples: | |
452 | /foo/ is a prefix of /foo <-- may seem odd, but desired | |
453 | /foo is a prefix of /foo/ | |
454 | / is a prefix of /foo/bar | |
455 | / is not a prefix of foo/bar | |
456 | foo/ is a prefix foo/bar | |
457 | /foo is not a prefix of /foobar | |
458 | */ | |
459 | ||
460 | static int | |
3c054baf | 461 | path_prefix_p (String const path1, String const path2) |
a351e48c | 462 | { |
3c054baf | 463 | size_t len1 = path1.size (); |
a351e48c | 464 | /* Handle case where PATH1 has trailing '/' and when it doesn't. */ |
3c054baf RC |
465 | if (len1 > 0 && SLASH_P (path1.cstr_oneuse ()[len1 - 1])) |
466 | --len1; | |
a351e48c CF |
467 | |
468 | if (len1 == 0) | |
3c054baf RC |
469 | return SLASH_P (path2.cstr_oneuse ()[0]) |
470 | && !SLASH_P (path2.cstr_oneuse ()[1]); | |
a351e48c | 471 | |
3c054baf | 472 | if (path1.casecompare (path2, len1) != 0) |
a351e48c CF |
473 | return 0; |
474 | ||
3c054baf RC |
475 | return SLASH_P (path2.cstr_oneuse ()[len1]) || path2.size () == len1 |
476 | || path1.cstr_oneuse ()[len1 - 1] == ':'; | |
a351e48c CF |
477 | } |
478 | ||
1ac649ed | 479 | String |
3c054baf | 480 | cygpath (String const &thePath) |
a351e48c | 481 | { |
3c054baf | 482 | size_t max_len = 0; |
b24c88b3 | 483 | struct mnt *m, *match = NULL; |
3c054baf | 484 | for (m = mount_table; m->posix.size (); m++) |
a351e48c | 485 | { |
3c054baf RC |
486 | size_t n = m->posix.size (); |
487 | if (n <= max_len || !path_prefix_p (m->posix, thePath)) | |
a351e48c CF |
488 | continue; |
489 | max_len = n; | |
490 | match = m; | |
491 | } | |
492 | ||
b24c88b3 RC |
493 | if (!match) |
494 | return NULL; | |
495 | ||
3c054baf RC |
496 | String native; |
497 | if (max_len == thePath.size ()) | |
498 | { | |
499 | native = match->native; | |
500 | } | |
a351e48c | 501 | else |
1ac649ed | 502 | native = match->native + "/" + String (thePath.cstr_oneuse() + max_len); |
a351e48c CF |
503 | return native; |
504 | } |