Leaked source code of windows server 2003
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

786 lines
22 KiB

  1. //+--------------------------------------------------------------------------
  2. //
  3. // Microsoft Windows
  4. // Copyright (C) Microsoft Corporation, 1996 - 1997.
  5. //
  6. // File: path.cxx
  7. //
  8. // Contents: Common routines for processing file and pathnames.
  9. //
  10. // History: 11-22-1996 DavidMun Created
  11. //
  12. //---------------------------------------------------------------------------
  13. #include "..\pch\headers.hxx"
  14. #pragma hdrstop
  15. #include <regstr.h> // for app path reg key constant
  16. #include "..\inc\common.hxx"
  17. #include "..\inc\misc.hxx"
  18. #include "..\inc\debug.hxx"
  19. //
  20. // Forward references
  21. //
  22. LPCTSTR
  23. FindFirstTrailingSpace(LPCTSTR ptsz);
  24. BOOL
  25. FileExistsInPath(
  26. LPTSTR ptszFilename,
  27. LPCTSTR ptszPath,
  28. LPTSTR ptszFoundFile,
  29. ULONG cchFoundBuf);
  30. //+--------------------------------------------------------------------------
  31. //
  32. // Function: ProcessApplicationName
  33. //
  34. // Synopsis: Canonicalize and search for [ptszFilename].
  35. //
  36. // Arguments: [ptszFilename] - must be at least MAX_PATH chars long
  37. // [tszWorkingDir] - "" or a directory not ending in slash
  38. //
  39. // Returns: TRUE - a filename was found
  40. // FALSE - no filename found
  41. //
  42. // Modifies: *[ptszFilename]
  43. //
  44. // History: 11-21-1996 DavidMun Created
  45. // 06-03-1997 DavidMun Expand environment vars
  46. //
  47. // Notes: This function should only be called to process filenames
  48. // on the local machine.
  49. //
  50. //---------------------------------------------------------------------------
  51. BOOL
  52. ProcessApplicationName(LPTSTR ptszFilename, size_t cchBuff, LPCTSTR tszWorkingDir)
  53. {
  54. BOOL fFound = FALSE;
  55. TCHAR tszFilenameWithExe[MAX_PATH + 1];
  56. //
  57. // Use tszFilenameWithExe as a temporary buffer for preparing the string
  58. // in ptszFilename. Get rid of lead/trail spaces and double quotes, then
  59. // expand environment strings.
  60. //
  61. StringCchCopy(tszFilenameWithExe, MAX_PATH + 1, ptszFilename);
  62. StripLeadTrailSpace(tszFilenameWithExe);
  63. DeleteQuotes(tszFilenameWithExe);
  64. ExpandEnvironmentStrings(tszFilenameWithExe, ptszFilename, MAX_PATH + 1);
  65. tszFilenameWithExe[0] = TEXT('\0');
  66. ULONG cchFilename = lstrlen(ptszFilename) + 1;
  67. //
  68. // If the filename doesn't end with .exe, and the resulting string
  69. // wouldn't be greater than MAX_PATH + 1 (including NULL),
  70. // create a version of the filename with .exe appended.
  71. //
  72. // Note this will prevent us from finding foo.exe.exe when we're given
  73. // foo.exe, but the performance gained by excluding this case outweighs
  74. // the value of completeness, since it's unlikely anyone would create
  75. // such a filename.
  76. //
  77. if (cchFilename < MAX_PATH + 1 - 4)
  78. {
  79. LPTSTR ptszLastDot = _tcsrchr(ptszFilename, TEXT('.'));
  80. if (!ptszLastDot || lstrcmpi(ptszLastDot, DOTEXE))
  81. {
  82. StringCchCopy(tszFilenameWithExe, MAX_PATH + 1, ptszFilename);
  83. StringCchCopy(&tszFilenameWithExe[cchFilename], MAX_PATH + 1 - cchFilename, DOTEXE);
  84. }
  85. }
  86. do
  87. {
  88. //
  89. // If the user specified path information (if there is a colon or
  90. // backslash anywhere in the string), look for the file as
  91. // specified or with .exe appended, but look nowhere else.
  92. //
  93. if (_tcspbrk(ptszFilename, TEXT(":\\")))
  94. {
  95. if (*tszFilenameWithExe)
  96. {
  97. fFound = FileExists(tszFilenameWithExe, MAX_PATH + 1);
  98. if (fFound)
  99. {
  100. StringCchCopy(ptszFilename, cchBuff, tszFilenameWithExe);
  101. }
  102. }
  103. if (!fFound)
  104. {
  105. fFound = FileExists(ptszFilename, cchBuff);
  106. }
  107. break;
  108. }
  109. //
  110. // First try the working directory
  111. //
  112. TCHAR tszFoundFile[MAX_PATH + 1] = TEXT("");
  113. if (*tszWorkingDir)
  114. {
  115. if (*tszFilenameWithExe)
  116. {
  117. fFound = FileExistsInPath(tszFilenameWithExe,
  118. tszWorkingDir,
  119. tszFoundFile,
  120. MAX_PATH + 1);
  121. }
  122. if (!fFound)
  123. {
  124. fFound = FileExistsInPath(ptszFilename,
  125. tszWorkingDir,
  126. tszFoundFile,
  127. MAX_PATH + 1);
  128. }
  129. if (fFound)
  130. {
  131. StringCchCopy(ptszFilename, cchBuff, tszFoundFile);
  132. break;
  133. }
  134. }
  135. //
  136. // Next try using the app paths key
  137. //
  138. TCHAR tszAppPathVar[MAX_PATH_VALUE] = TEXT("");
  139. TCHAR tszAppPathDefault[MAX_PATH + 1] = TEXT("");
  140. if (*tszFilenameWithExe)
  141. {
  142. GetAppPathInfo(tszFilenameWithExe,
  143. tszAppPathDefault,
  144. MAX_PATH + 1,
  145. tszAppPathVar,
  146. MAX_PATH_VALUE);
  147. }
  148. if (!*tszAppPathDefault && !*tszAppPathVar)
  149. {
  150. GetAppPathInfo(ptszFilename,
  151. tszAppPathDefault,
  152. MAX_PATH + 1,
  153. tszAppPathVar,
  154. MAX_PATH_VALUE);
  155. }
  156. //
  157. // If there was a default value, try that
  158. //
  159. if (*tszAppPathDefault)
  160. {
  161. fFound = FileExists(tszAppPathDefault, MAX_PATH + 1);
  162. if (fFound)
  163. {
  164. StringCchCopy(ptszFilename, cchBuff, tszAppPathDefault);
  165. break;
  166. }
  167. //
  168. // If there's room, concat .exe to the default and look for
  169. // that
  170. //
  171. ULONG cchDefault = lstrlen(tszAppPathDefault) + 1;
  172. if (cchDefault < MAX_PATH + 1 - 4)
  173. {
  174. StringCchCat(tszAppPathDefault, MAX_PATH + 1, DOTEXE);
  175. fFound = FileExists(tszAppPathDefault, MAX_PATH + 1);
  176. if (fFound)
  177. {
  178. StringCchCopy(ptszFilename, cchBuff, tszAppPathDefault);
  179. break;
  180. }
  181. }
  182. }
  183. //
  184. // If the app path key specified a value for the PATH variable,
  185. // try looking in all the directories it specifies
  186. //
  187. if (*tszAppPathVar)
  188. {
  189. if (*tszFilenameWithExe)
  190. {
  191. fFound = FileExistsInPath(tszFilenameWithExe,
  192. tszAppPathVar,
  193. tszFoundFile,
  194. MAX_PATH + 1);
  195. }
  196. if (!fFound)
  197. {
  198. fFound = FileExistsInPath(ptszFilename,
  199. tszAppPathVar,
  200. tszFoundFile,
  201. MAX_PATH + 1);
  202. }
  203. if (fFound)
  204. {
  205. StringCchCopy(ptszFilename, cchBuff, tszFoundFile);
  206. break;
  207. }
  208. }
  209. //
  210. // Try looking along the system PATH variable
  211. //
  212. ULONG cchPath;
  213. TCHAR tszSystemPath[MAX_PATH_VALUE] = TEXT("");
  214. cchPath = GetEnvironmentVariable(TEXT("Path"),
  215. tszSystemPath,
  216. MAX_PATH_VALUE);
  217. if (!cchPath || cchPath > MAX_PATH_VALUE)
  218. {
  219. break;
  220. }
  221. if (*tszFilenameWithExe)
  222. {
  223. fFound = FileExistsInPath(tszFilenameWithExe,
  224. tszSystemPath,
  225. tszFoundFile,
  226. MAX_PATH + 1);
  227. }
  228. if (!fFound)
  229. {
  230. fFound = FileExistsInPath(ptszFilename,
  231. tszSystemPath,
  232. tszFoundFile,
  233. MAX_PATH + 1);
  234. }
  235. if (fFound)
  236. {
  237. StringCchCopy(ptszFilename, cchBuff, tszFoundFile);
  238. }
  239. } while (0);
  240. return fFound;
  241. }
  242. //+--------------------------------------------------------------------------
  243. //
  244. // Function: IsLocalFilename
  245. //
  246. // Synopsis: Return TRUE if [tszFilename] represents a file on the local
  247. // machine, FALSE otherwise.
  248. //
  249. // History: 1-31-1997 DavidMun Created
  250. //
  251. //---------------------------------------------------------------------------
  252. BOOL
  253. IsLocalFilename(LPCTSTR tszFilename)
  254. {
  255. if (!tszFilename || !*tszFilename)
  256. {
  257. return FALSE;
  258. }
  259. if (tszFilename[0] == TEXT('\\') && tszFilename[1] == TEXT('\\'))
  260. {
  261. //
  262. // Find the length of the portion of the name belonging to the machine name
  263. //
  264. LPCTSTR ptszNextSlash = _tcschr(tszFilename + 2, TEXT('\\'));
  265. if (!ptszNextSlash)
  266. {
  267. return FALSE;
  268. }
  269. DWORD cchMachineName = (DWORD)(ptszNextSlash - tszFilename - 2);
  270. //
  271. // Get the local machine name (both NetBIOS and FQDN) to compare with that passed in.
  272. //
  273. TCHAR tszLocalName[SA_MAX_COMPUTERNAME_LENGTH + 1];
  274. DWORD cchLocalName = SA_MAX_COMPUTERNAME_LENGTH + 1;
  275. if (!GetComputerName(tszLocalName, &cchLocalName))
  276. {
  277. ERR_OUT("IsLocalFilename: GetComputerName", HRESULT_FROM_WIN32(GetLastError()));
  278. return FALSE;
  279. }
  280. TCHAR tszFQDN[SA_MAX_COMPUTERNAME_LENGTH + 1];
  281. DWORD cchFQDN = SA_MAX_COMPUTERNAME_LENGTH + 1;
  282. if (!GetComputerNameEx(ComputerNamePhysicalDnsFullyQualified, tszFQDN, &cchFQDN))
  283. {
  284. ERR_OUT("IsLocalFilename: GetComputerNameEx", HRESULT_FROM_WIN32(GetLastError()));
  285. return FALSE;
  286. }
  287. //
  288. // Return whether we have a match on the machine name portion.
  289. // I'm assuming that we won't have a case where the NetBIOS name
  290. // and the FQDN are the same length but different.
  291. //
  292. if (cchMachineName == cchLocalName)
  293. {
  294. return lstrcmpi(tszFilename + 2, tszLocalName) == 0;
  295. }
  296. else if (cchMachineName == cchFQDN)
  297. {
  298. return lstrcmpi(tszFilename + 2, tszFQDN) == 0;
  299. }
  300. else
  301. {
  302. // if the lengths didn't match, there's no need
  303. // to even bother with a string comparison
  304. return FALSE;
  305. }
  306. }
  307. if (s_isDriveLetter(tszFilename[0]) && tszFilename[1] == TEXT(':'))
  308. {
  309. TCHAR tszRoot[] = TEXT("x:\\");
  310. tszRoot[0] = tszFilename[0];
  311. UINT uiType = GetDriveType(tszRoot);
  312. if (uiType == DRIVE_REMOTE || uiType == 0 || uiType == 1)
  313. {
  314. return FALSE;
  315. }
  316. }
  317. return TRUE;
  318. }
  319. //+--------------------------------------------------------------------------
  320. //
  321. // Function: StripLeadTrailSpace
  322. //
  323. // Synopsis: Delete leading and trailing spaces from [ptsz].
  324. //
  325. // History: 11-22-1996 DavidMun Created
  326. //
  327. //---------------------------------------------------------------------------
  328. VOID
  329. StripLeadTrailSpace(LPTSTR ptsz)
  330. {
  331. ULONG cchLeadingSpace = _tcsspn(ptsz, TEXT(" \t"));
  332. ULONG cch = lstrlen(ptsz);
  333. //
  334. // If there are any leading spaces or tabs, move the string
  335. // (including its nul terminator) left to delete them.
  336. //
  337. if (cchLeadingSpace)
  338. {
  339. MoveMemory(ptsz,
  340. ptsz + cchLeadingSpace,
  341. (cch - cchLeadingSpace + 1) * sizeof(TCHAR));
  342. cch -= cchLeadingSpace;
  343. }
  344. //
  345. // Concatenate at the first trailing space
  346. //
  347. LPTSTR ptszTrailingSpace = (LPTSTR) FindFirstTrailingSpace(ptsz);
  348. if (ptszTrailingSpace)
  349. {
  350. *ptszTrailingSpace = TEXT('\0');
  351. }
  352. }
  353. //+--------------------------------------------------------------------------
  354. //
  355. // Function: FindFirstTrailingSpace
  356. //
  357. // Synopsis: Return a pointer to the first trailing space in [ptsz].
  358. //
  359. // History: 11-22-1996 DavidMun Created
  360. //
  361. //---------------------------------------------------------------------------
  362. LPCTSTR
  363. FindFirstTrailingSpace(LPCTSTR ptsz)
  364. {
  365. LPCTSTR ptszFirstTrailingSpace = NULL;
  366. LPCTSTR ptszCur;
  367. for (ptszCur = ptsz; *ptszCur; ptszCur= NextChar(ptszCur))
  368. {
  369. if (*ptszCur == ' ' || *ptszCur == '\t')
  370. {
  371. if (!ptszFirstTrailingSpace)
  372. {
  373. ptszFirstTrailingSpace = ptszCur;
  374. }
  375. }
  376. else if (ptszFirstTrailingSpace)
  377. {
  378. ptszFirstTrailingSpace = NULL;
  379. }
  380. }
  381. return ptszFirstTrailingSpace;
  382. }
  383. //+--------------------------------------------------------------------------
  384. //
  385. // Function: DeleteQuotes
  386. //
  387. // Synopsis: Delete all instances of the double quote character from
  388. // [ptsz].
  389. //
  390. // Arguments: [ptsz] - nul terminated string
  391. //
  392. // Modifies: *[ptsz]
  393. //
  394. // History: 11-21-1996 DavidMun Created
  395. //
  396. //---------------------------------------------------------------------------
  397. VOID
  398. DeleteQuotes(LPTSTR ptsz)
  399. {
  400. TCHAR *ptszLead;
  401. TCHAR *ptszTrail;
  402. //
  403. // Move a lead and trail pointer through the buffer, copying from the lead
  404. // to the trail whenever the character isn't one we're deleting (a double
  405. // quote).
  406. //
  407. // Note: the "Lead" and "Trail" in ptszLead and ptszTrail do not refer
  408. // to DBCS lead/trail bytes, rather that the ptszLead pointer can move
  409. // ahead of ptszTrail when it is advanced past a double quote.
  410. //
  411. for (ptszTrail = ptszLead = ptsz;
  412. *ptszLead;
  413. ptszLead = NextChar(ptszLead))
  414. {
  415. //
  416. // If the current char is a double quote, we want it deleted, so don't
  417. // copy it and go on to the next char.
  418. //
  419. if (*ptszLead == TEXT('"'))
  420. {
  421. continue;
  422. }
  423. //
  424. // ptszLead is pointing to a 'normal' character, i.e. not a double
  425. // quote.
  426. //
  427. *ptszTrail++ = ptszLead[0];
  428. }
  429. *ptszTrail = TEXT('\0');
  430. }
  431. //+--------------------------------------------------------------------------
  432. //
  433. // Function: AddQuotes
  434. //
  435. // Synopsis: If there's room in the buffer, insert a quote as the first
  436. // character and concat a quote as the last character.
  437. //
  438. // Arguments: [ptsz] - string to modify
  439. // [cchBuf] - size of string's buffer, in chars
  440. //
  441. // History: 11-22-1996 DavidMun Created
  442. //
  443. //---------------------------------------------------------------------------
  444. VOID
  445. AddQuotes(LPTSTR ptsz, ULONG cchBuf)
  446. {
  447. ULONG cch = lstrlen(ptsz);
  448. if (cch < cchBuf - 2)
  449. {
  450. MoveMemory(ptsz + 1, ptsz, cch * sizeof(TCHAR));
  451. *ptsz = ptsz[cch + 1] = TEXT('"');
  452. ptsz[cch + 2] = TEXT('\0');
  453. }
  454. }
  455. //+---------------------------------------------------------------------------
  456. //
  457. // Function: FileExists
  458. //
  459. // Synopsis: Return TRUE if the specified file exists and is not a
  460. // directory.
  461. //
  462. // Arguments: [ptszFileName] - filename to search for & modify
  463. // [cchBuff] - size of buffer
  464. //
  465. // Modifies: Filename portion of [ptszFileName].
  466. //
  467. // Returns: TRUE - file found
  468. // FALSE - file not found or error
  469. //
  470. // History: 11-21-96 DavidMun Created
  471. //
  472. //----------------------------------------------------------------------------
  473. BOOL
  474. FileExists(LPTSTR ptszFileName, size_t cchBuff)
  475. {
  476. TCHAR tszFullPath[MAX_PATH + 1];
  477. LPTSTR ptszFilePart;
  478. ULONG cchFullPath = GetFullPathName(ptszFileName,
  479. MAX_PATH + 1,
  480. tszFullPath,
  481. &ptszFilePart) + 1;
  482. if (cchFullPath && cchFullPath <= MAX_PATH + 1)
  483. {
  484. if (FAILED(StringCchCopy(ptszFileName, cchBuff, tszFullPath)))
  485. {
  486. return FALSE;
  487. }
  488. }
  489. else
  490. {
  491. return FALSE;
  492. }
  493. ULONG flAttributes;
  494. flAttributes = GetFileAttributes(ptszFileName);
  495. // If we couldn't determine file's attributes, don't consider it found
  496. if (flAttributes == 0xFFFFFFFF)
  497. {
  498. return FALSE;
  499. }
  500. // if file is actually a directory, it's unsuitable as a task, so fail
  501. if (flAttributes & FILE_ATTRIBUTE_DIRECTORY)
  502. {
  503. return FALSE;
  504. }
  505. // Get the filename sans trailing spaces
  506. WIN32_FIND_DATA FindFileData;
  507. HANDLE hFile = FindFirstFile(ptszFileName, &FindFileData);
  508. if (hFile == INVALID_HANDLE_VALUE)
  509. {
  510. return FALSE;
  511. }
  512. FindClose(hFile);
  513. LPTSTR ptszLastSlash = _tcsrchr((LPTSTR)ptszFileName, TEXT('\\'));
  514. if (ptszLastSlash)
  515. {
  516. StringCchCopy(ptszLastSlash + 1, cchBuff - (ptszLastSlash + 1 - ptszFileName), FindFileData.cFileName);
  517. }
  518. return TRUE;
  519. }
  520. //+--------------------------------------------------------------------------
  521. //
  522. // Function: FileExistsInPath
  523. //
  524. // Synopsis: Return TRUE if [ptszFilename] exists in path [ptszPath].
  525. //
  526. // Arguments: [ptszFilename] - file to look for
  527. // [ptszPath] - semicolon delimited list of dirs
  528. // [ptszFoundFile] - if found, [ptszDir]\[ptszFilename]
  529. // [cchFoundBuf] - size in chars of [ptszFoundFile] buffer
  530. //
  531. // Returns: TRUE if file found in dir, FALSE otherwise
  532. //
  533. // Modifies: *[ptszFoundFile]
  534. //
  535. // History: 11-22-1996 DavidMun Created
  536. //
  537. // Notes: Note that by calling FileExists we ensure the found file
  538. // is a file, not a directory.
  539. //
  540. //---------------------------------------------------------------------------
  541. BOOL
  542. FileExistsInPath(
  543. LPTSTR ptszFilename,
  544. LPCTSTR ptszPath,
  545. LPTSTR ptszFoundFile,
  546. ULONG cchFoundBuf)
  547. {
  548. ULONG cchCopied;
  549. LPTSTR ptszFilePart;
  550. cchCopied = SearchPath(ptszPath,
  551. ptszFilename,
  552. NULL,
  553. cchFoundBuf,
  554. ptszFoundFile,
  555. &ptszFilePart);
  556. if (cchCopied && cchCopied <= cchFoundBuf)
  557. {
  558. return FileExists(ptszFoundFile, cchFoundBuf);
  559. }
  560. return FALSE;
  561. }
  562. #define MAX_KEY_LEN (ARRAY_LEN(REGSTR_PATH_APPPATHS) + MAX_PATH)
  563. //+--------------------------------------------------------------------------
  564. //
  565. // Function: GetAppPathInfo
  566. //
  567. // Synopsis: Fill [ptszAppPathDefault] with the default value and
  568. // [ptszAppPathVar] with the Path value in the
  569. // [ptszFilename] application's key under the APPPATHS regkey.
  570. //
  571. // Arguments: [ptszFilename] - application name
  572. // [ptszAppPathDefault] - if not NULL, filled with default value
  573. // [cchDefaultBuf] - size of [ptszAppPathDefault] buffer
  574. // [ptszAppPathVar] - if not NULL, filled with Path value
  575. // [cchPathVarBuf] - size of [cchPathVarBuf] buffer
  576. //
  577. // Modifies: *[ptszAppPathDefault], *[ptszAppPathVar]
  578. //
  579. // History: 11-22-1996 DavidMun Created
  580. //
  581. // Notes: Both values are optional on the registry key, so if a
  582. // requested value isn't found, it is set to "".
  583. //
  584. //---------------------------------------------------------------------------
  585. VOID
  586. GetAppPathInfo(
  587. LPCTSTR ptszFilename,
  588. LPTSTR ptszAppPathDefault,
  589. ULONG cchDefaultBuf,
  590. LPTSTR ptszAppPathVar,
  591. ULONG cchPathVarBuf)
  592. {
  593. HKEY hkey = NULL;
  594. TCHAR tszAppPathKey[MAX_KEY_LEN];
  595. //
  596. // Initialize out vars
  597. //
  598. if (ptszAppPathDefault)
  599. {
  600. ptszAppPathDefault[0] = TEXT('\0');
  601. }
  602. if (ptszAppPathVar)
  603. {
  604. ptszAppPathVar[0] = TEXT('\0');
  605. }
  606. //
  607. // Build registry key name for this app
  608. //
  609. StringCchCopy(tszAppPathKey, MAX_KEY_LEN, REGSTR_PATH_APPPATHS);
  610. StringCchCat(tszAppPathKey, MAX_KEY_LEN, TEXT("\\"));
  611. StringCchCat(tszAppPathKey, MAX_KEY_LEN, ptszFilename);
  612. do
  613. {
  614. LRESULT lr;
  615. lr = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
  616. tszAppPathKey,
  617. 0,
  618. KEY_QUERY_VALUE,
  619. &hkey);
  620. if (lr != ERROR_SUCCESS)
  621. {
  622. break;
  623. }
  624. //
  625. // If the key could be opened, attempt to read requested values.
  626. // Both are optional, so ignore errors.
  627. //
  628. DWORD cb;
  629. DWORD dwType;
  630. if (ptszAppPathDefault)
  631. {
  632. cb = cchDefaultBuf * sizeof(TCHAR);
  633. lr = RegQueryValueEx(hkey,
  634. NULL, // value name
  635. NULL, // reserved
  636. &dwType,
  637. (LPBYTE) ptszAppPathDefault,
  638. &cb);
  639. if (lr == ERROR_SUCCESS)
  640. {
  641. schAssert(dwType == REG_SZ);
  642. }
  643. }
  644. if (ptszAppPathVar)
  645. {
  646. cb = cchPathVarBuf * sizeof(TCHAR);
  647. lr = RegQueryValueEx(hkey,
  648. TEXT("Path"), // value name
  649. NULL, // reserved
  650. &dwType,
  651. (LPBYTE) ptszAppPathVar,
  652. &cb);
  653. if (lr == ERROR_SUCCESS)
  654. {
  655. schAssert(dwType == REG_SZ);
  656. }
  657. }
  658. } while (0);
  659. if (hkey)
  660. {
  661. RegCloseKey(hkey);
  662. }
  663. }