b3f2c867c3032cbb4359bf8093c20e55283487cb
[platform/upstream/dotnet/runtime.git] / src / coreclr / hosts / corerun / corerun.cpp
1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3
4 // Runtime headers
5 #include <coreclrhost.h>
6
7 #include "corerun.hpp"
8
9 using char_t = pal::char_t;
10 using string_t = pal::string_t;
11
12 struct configuration
13 {
14     configuration() = default;
15     configuration(const configuration&) = delete;
16     configuration(configuration&&) = delete;
17     configuration& operator=(const configuration&) = delete;
18     configuration& operator=(configuration&&) = delete;
19
20     ~configuration()
21     {
22         for (int i = 0; i < entry_assembly_argc; ++i)
23         {
24             ::free((void*)entry_assembly_argv[i]);
25         }
26         ::free(entry_assembly_argv);
27     }
28
29     //
30     // Settings
31     //
32
33     // CLR path - user supplied location of coreclr binary and managed assemblies.
34     string_t clr_path;
35
36     // The full path to the Supplied managed entry assembly.
37     string_t entry_assembly_fullpath;
38
39     // Arguments to pass to managed entry assembly.
40     int entry_assembly_argc;
41     const char_t** entry_assembly_argv;
42
43     // Collection of user-defined key/value pairs that will be appended
44     // to the initialization of the runtime.
45     std::vector<string_t> user_defined_keys;
46     std::vector<string_t> user_defined_values;
47
48     // Wait for debugger to be attached.
49     bool wait_to_debug;
50
51     // Perform self test.
52     bool self_test;
53 };
54
55 namespace envvar
56 {
57     // Points to a path containing the CoreCLR binary.
58     const char_t* coreRoot = W("CORE_ROOT");
59
60     // Points to a path containing additional platform assemblies.
61     const char_t* coreLibraries = W("CORE_LIBRARIES");
62
63     // Variable used to preload a mock hostpolicy for testing.
64     const char_t* mockHostPolicy = W("MOCK_HOSTPOLICY");
65 }
66
67 static void wait_for_debugger()
68 {
69     pal::debugger_state_t state = pal::is_debugger_attached();
70     if (state == pal::debugger_state_t::na)
71     {
72         pal::fprintf(stdout, W("Debugger attach is not available on this platform\n"));
73         return;
74     }
75     else if (state == pal::debugger_state_t::not_attached)
76     {
77         uint32_t pid = pal::get_process_id();
78         pal::fprintf(stdout, W("Waiting for the debugger to attach (PID: %u). Press any key to continue ...\n"), pid);
79         (void)getchar();
80         state = pal::is_debugger_attached();
81     }
82
83     if (state == pal::debugger_state_t::attached)
84     {
85         pal::fprintf(stdout, W("Debugger is attached.\n"));
86     }
87     else
88     {
89         pal::fprintf(stdout, W("Debugger failed to attach.\n"));
90     }
91 }
92
93 // N.B. It seems that CoreCLR doesn't always use the first instance of an assembly on the TPA list
94 // (for example, ni's may be preferred over il, even if they appear later). Therefore, when building
95 // the TPA only include the first instance of a simple assembly name to allow users the opportunity to
96 // override Framework assemblies by placing dlls in %CORE_LIBRARIES%.
97 static string_t build_tpa(const string_t& core_root, const string_t& core_libraries)
98 {
99     static const char_t* const tpa_extensions[] =
100     {
101         W(".ni.dll"),  // Probe for .ni.dll first so that it's preferred if ni and il coexist in the same dir
102         W(".dll"),
103         W(".ni.exe"),
104         W(".exe"),
105         nullptr
106     };
107
108     std::set<string_t> name_set;
109     pal::stringstream_t tpa_list;
110
111     // Iterate over all extensions.
112     for (const char_t* const* curr_ext = tpa_extensions; *curr_ext != nullptr; ++curr_ext)
113     {
114         const char_t* ext = *curr_ext;
115         const size_t ext_len = pal::strlen(ext);
116
117         // Iterate over all supplied directories.
118         for (const string_t& dir : { core_libraries, core_root })
119         {
120             if (dir.empty())
121                 continue;
122
123             assert(dir.back() == pal::dir_delim);
124             string_t tmp = pal::build_file_list(dir, ext, [&](const char_t* file)
125                 {
126                     string_t file_local{ file };
127
128                     // Strip the extension.
129                     if (pal::string_ends_with(file_local, ext_len, ext))
130                         file_local = file_local.substr(0, file_local.length() - ext_len);
131
132                     // Return true if the file is new.
133                     return name_set.insert(file_local).second;
134                 });
135
136             // Add to the TPA.
137             tpa_list << tmp;
138         }
139     }
140
141     return tpa_list.str();
142 }
143
144 static bool try_get_export(pal::mod_t mod, const char* symbol, void** fptr)
145 {
146     assert(mod != nullptr && symbol != nullptr && fptr != nullptr);
147     *fptr = pal::get_module_symbol(mod, symbol);
148     if (*fptr != nullptr)
149         return true;
150
151     pal::fprintf(stderr, W("Export '%s' not found.\n"), symbol);
152     return false;
153 }
154
155 class logger_t final
156 {
157     const char* _exePath;
158     int _propertyCount;
159     const char** _propertyKeys;
160     const char** _propertyValues;
161     const char* _managedAssembly;
162     int _argc;
163     const char** _argv;
164 public:
165     logger_t(
166         const char* exePath,
167         int propertyCount, const char** propertyKeys, const char** propertyValues,
168         const char* managedAssembly, int argc, const char** argv)
169     : _exePath{ exePath }
170     , _propertyCount{ propertyCount }
171     , _propertyKeys{ propertyKeys }
172     , _propertyValues{ propertyValues }
173     , _managedAssembly{ managedAssembly }
174     , _argc{ argc }
175     , _argv{ argv }
176     { }
177
178     void dump_details(FILE* fd = stdout)
179     {
180         // Using std::fprintf since values have been converted to UTF-8.
181         std::fprintf(fd, "Exe path: %s\n", _exePath);
182         std::fprintf(fd, "Properties:\n");
183         for (int i = 0; i < _propertyCount; ++i)
184         {
185             std::fprintf(fd, "    %s = %s\n", _propertyKeys[i], _propertyValues[i]);
186         }
187
188         std::fprintf(fd, "Managed assembly: %s\n", _managedAssembly);
189         std::fprintf(fd, "Arguments (%d): ", _argc);
190         for (int i = 0; i < _argc; ++i)
191         {
192             std::fprintf(fd, "%s ", _argv[i]);
193         }
194         std::fprintf(fd, "\n");
195     }
196 };
197
198 // The current CoreCLR instance details.
199 static void* CurrentClrInstance;
200 static unsigned int CurrentAppDomainId;
201
202 static int run(const configuration& config)
203 {
204     platform_specific_actions actions;
205
206     // Check if debugger attach scenario was requested.
207     if (config.wait_to_debug)
208         wait_for_debugger();
209
210     string_t exe_path = pal::get_exe_path();
211
212     // Determine the managed application's path.
213     string_t app_path;
214     {
215         string_t file;
216         pal::split_path_to_dir_filename(config.entry_assembly_fullpath, app_path, file);
217         pal::ensure_trailing_delimiter(app_path);
218     }
219
220     // Define the NI app_path.
221     string_t app_path_ni = app_path + W("NI");
222     pal::ensure_trailing_delimiter(app_path_ni);
223     app_path_ni.append(1, pal::env_path_delim);
224     app_path_ni.append(app_path);
225
226     // Accumulate path for native search path.
227     pal::stringstream_t native_search_dirs;
228     native_search_dirs << app_path << pal::env_path_delim;
229
230     // CORE_LIBRARIES
231     string_t core_libs = pal::getenv(envvar::coreLibraries);
232     if (!core_libs.empty() && core_libs != app_path)
233     {
234         pal::ensure_trailing_delimiter(core_libs);
235         native_search_dirs << core_libs << pal::env_path_delim;
236     }
237
238     // Determine CORE_ROOT.
239     // Check if the path is user supplied and if not try
240     // the CORE_ROOT environment variable.
241     string_t core_root = !config.clr_path.empty()
242         ? config.clr_path
243         : pal::getenv(envvar::coreRoot);
244
245     // If CORE_ROOT wasn't supplied use the exe binary path, otherwise
246     // ensure path is valid and add to native search path.
247     if (core_root.empty())
248     {
249         string_t file;
250         pal::split_path_to_dir_filename(exe_path, core_root, file);
251         pal::ensure_trailing_delimiter(core_root);
252     }
253     else
254     {
255         pal::ensure_trailing_delimiter(core_root);
256         native_search_dirs << core_root << pal::env_path_delim;
257     }
258
259     string_t tpa_list = build_tpa(core_root, core_libs);
260
261     {
262         // Load hostpolicy if requested.
263         string_t mock_hostpolicy = pal::getenv(envvar::mockHostPolicy);
264         if (!mock_hostpolicy.empty()
265             && !pal::try_load_hostpolicy(mock_hostpolicy))
266         {
267             return -1;
268         }
269     }
270
271     actions.before_coreclr_load();
272
273     // Attempt to load CoreCLR.
274     pal::mod_t coreclr_mod;
275     if (!pal::try_load_coreclr(core_root, coreclr_mod))
276     {
277         return -1;
278     }
279
280     // Get CoreCLR exports
281     coreclr_initialize_ptr coreclr_init_func = nullptr;
282     coreclr_execute_assembly_ptr coreclr_execute_func = nullptr;
283     coreclr_shutdown_2_ptr coreclr_shutdown2_func = nullptr;
284     if (!try_get_export(coreclr_mod, "coreclr_initialize", (void**)&coreclr_init_func)
285         || !try_get_export(coreclr_mod, "coreclr_execute_assembly", (void**)&coreclr_execute_func)
286         || !try_get_export(coreclr_mod, "coreclr_shutdown_2", (void**)&coreclr_shutdown2_func))
287     {
288         return -1;
289     }
290
291     // Construct CoreCLR properties.
292     pal::string_utf8_t tpa_list_utf8 = pal::convert_to_utf8(std::move(tpa_list));
293     pal::string_utf8_t app_path_utf8 = pal::convert_to_utf8(std::move(app_path));
294     pal::string_utf8_t app_path_ni_utf8 = pal::convert_to_utf8(std::move(app_path_ni));
295     pal::string_utf8_t native_search_dirs_utf8 = pal::convert_to_utf8(native_search_dirs.str());
296
297     std::vector<pal::string_utf8_t> user_defined_keys_utf8;
298     std::vector<pal::string_utf8_t> user_defined_values_utf8;
299     for (const string_t& str : config.user_defined_keys)
300         user_defined_keys_utf8.push_back(pal::convert_to_utf8(str.c_str()));
301     for (const string_t& str : config.user_defined_values)
302         user_defined_values_utf8.push_back(pal::convert_to_utf8(str.c_str()));
303
304     // Set base initialization properties.
305     std::vector<const char*> propertyKeys;
306     std::vector<const char*> propertyValues;
307
308     // TRUSTED_PLATFORM_ASSEMBLIES
309     // - The list of complete paths to each of the fully trusted assemblies
310     propertyKeys.push_back("TRUSTED_PLATFORM_ASSEMBLIES");
311     propertyValues.push_back(tpa_list_utf8.c_str());
312
313     // APP_PATHS
314     // - The list of paths which will be probed by the assembly loader
315     propertyKeys.push_back("APP_PATHS");
316     propertyValues.push_back(app_path_utf8.c_str());
317
318     // APP_NI_PATHS
319     // - The list of additional paths that the assembly loader will probe for ngen images
320     propertyKeys.push_back("APP_NI_PATHS");
321     propertyValues.push_back(app_path_ni_utf8.c_str());
322
323     // NATIVE_DLL_SEARCH_DIRECTORIES
324     // - The list of paths that will be probed for native DLLs called by PInvoke
325     propertyKeys.push_back("NATIVE_DLL_SEARCH_DIRECTORIES");
326     propertyValues.push_back(native_search_dirs_utf8.c_str());
327
328     // Sanity check before adding user-defined properties
329     assert(propertyKeys.size() == propertyValues.size());
330
331     // Insert user defined properties
332     for (const pal::string_utf8_t& str : user_defined_keys_utf8)
333         propertyKeys.push_back(str.c_str());
334     for (const pal::string_utf8_t& str : user_defined_values_utf8)
335         propertyValues.push_back(str.c_str());
336
337     assert(propertyKeys.size() == propertyValues.size());
338     int propertyCount = (int)propertyKeys.size();
339
340     // Construct arguments
341     pal::string_utf8_t exe_path_utf8 = pal::convert_to_utf8(std::move(exe_path));
342     std::vector<pal::string_utf8_t> argv_lifetime;
343     pal::malloc_ptr<const char*> argv_utf8{ pal::convert_argv_to_utf8(config.entry_assembly_argc, config.entry_assembly_argv, argv_lifetime) };
344     pal::string_utf8_t entry_assembly_utf8 = pal::convert_to_utf8(config.entry_assembly_fullpath.c_str());
345
346     logger_t logger{
347         exe_path_utf8.c_str(),
348         propertyCount, propertyKeys.data(), propertyValues.data(),
349         entry_assembly_utf8.c_str(), config.entry_assembly_argc, argv_utf8.get() };
350
351     int result;
352     result = coreclr_init_func(
353         exe_path_utf8.c_str(),
354         "corerun",
355         propertyCount,
356         propertyKeys.data(),
357         propertyValues.data(),
358         &CurrentClrInstance,
359         &CurrentAppDomainId);
360     if (FAILED(result))
361     {
362         pal::fprintf(stderr, W("BEGIN: coreclr_initialize failed - Error: 0x%08x\n"), result);
363         logger.dump_details();
364         pal::fprintf(stderr, W("END: coreclr_initialize failed - Error: 0x%08x\n"), result);
365         return -1;
366     }
367
368     int exit_code;
369     {
370         actions.before_execute_assembly(config.entry_assembly_fullpath);
371
372         result = coreclr_execute_func(
373             CurrentClrInstance,
374             CurrentAppDomainId,
375             config.entry_assembly_argc,
376             argv_utf8.get(),
377             entry_assembly_utf8.c_str(),
378             (uint32_t*)&exit_code);
379         if (FAILED(result))
380         {
381             pal::fprintf(stderr, W("BEGIN: coreclr_execute_assembly failed - Error: 0x%08x\n"), result);
382             logger.dump_details();
383             pal::fprintf(stderr, W("END: coreclr_execute_assembly failed - Error: 0x%08x\n"), result);
384             return -1;
385         }
386
387         actions.after_execute_assembly();
388     }
389
390     int latched_exit_code = 0;
391     result = coreclr_shutdown2_func(CurrentClrInstance, CurrentAppDomainId, &latched_exit_code);
392     if (FAILED(result))
393     {
394         pal::fprintf(stderr, W("coreclr_shutdown_2 failed - Error: 0x%08x\n"), result);
395         exit_code = -1;
396     }
397
398     if (exit_code != -1)
399         exit_code = latched_exit_code;
400
401     return exit_code;
402 }
403
404 // Display the command line options
405 static void display_usage()
406 {
407     pal::fprintf(
408         stderr,
409         W("USAGE: corerun [OPTIONS] assembly [ARGUMENTS]\n")
410         W("\n")
411         W("Execute the managed assembly with the passed in arguments\n")
412         W("\n")
413         W("Options:\n")
414         W("  -c, --clr-path - path to CoreCLR binary and managed CLR assemblies.\n")
415         W("  -p, --property - Property to pass to runtime during initialization.\n")
416         W("                   If a property value contains spaces, quote the entire argument.\n")
417         W("                   May be supplied multiple times. Format: <key>=<value>.\n")
418         W("  -d, --debug - causes corerun to wait for a debugger to attach before executing.\n")
419         W("  -?, -h, --help - show this help.\n")
420         W("\n")
421         W("The runtime binary is searched for in --clr-path, CORE_ROOT environment variable, then\n")
422         W("in the directory the corerun binary is located.\n")
423         W("\n")
424         W("Example:\n")
425         W("Wait for a debugger to attach, provide 2 additional properties for .NET\n")
426         W("runtime initialization, and pass an argument to the HelloWorld.dll assembly.\n")
427         W("  corerun -d -p System.GC.Concurrent=true -p \"FancyProp=/usr/first last/root\" HelloWorld.dll arg1\n")
428         );
429 }
430
431 // Parse the command line arguments
432 static bool parse_args(
433     const int argc,
434     const char_t* argv[],
435     configuration& config)
436 {
437     // The command line must contain at least the current exe name and the managed assembly path.
438     if (argc < 2)
439     {
440         display_usage();
441         return false;
442     }
443
444     for (int i = 1; i < argc; i++)
445     {
446         bool is_option = pal::is_cli_option(argv[i][0]);
447
448         // First argument that is not an option is the managed assembly to execute.
449         if (!is_option)
450         {
451             if (pal::strcmp(argv[i], W("exec")) == 0)
452             {
453                 continue;
454             }
455
456             config.entry_assembly_fullpath = pal::get_absolute_path(argv[i]);
457             i++; // Move to next argument.
458
459             config.entry_assembly_argc = argc - i;
460             config.entry_assembly_argv = (const char_t**)::malloc(config.entry_assembly_argc * sizeof(const char_t*));
461             assert(config.entry_assembly_argv != nullptr);
462             for (int c = 0; c < config.entry_assembly_argc; ++c)
463             {
464                 config.entry_assembly_argv[c] = pal::strdup(argv[i + c]);
465             }
466
467             // Successfully parsed arguments.
468             return true;
469         }
470
471         const char_t* arg = argv[i];
472         size_t arg_len = pal::strlen(arg);
473         if (arg_len == 1)
474         {
475             pal::fprintf(stderr, W("Option %s: invalid form\n"), arg);
476             break; // Invalid option
477         }
478
479         const char_t* option = arg + 1;
480         if (option[0] == W('-')) // Handle double '--'
481             option++;
482
483         // Path to core_root
484         if (pal::strcmp(option, W("c")) == 0 || (pal::strcmp(option, W("clr-path")) == 0))
485         {
486             i++;
487             if (i < argc)
488             {
489                 config.clr_path = argv[i];
490             }
491             else
492             {
493                 pal::fprintf(stderr, W("Option %s: missing path\n"), arg);
494                 break;
495             }
496         }
497         else if (pal::strcmp(option, W("p")) == 0 || (pal::strcmp(option, W("property")) == 0))
498         {
499             i++;
500             if (i >= argc)
501             {
502                 pal::fprintf(stderr, W("Option %s: missing property\n"), arg);
503                 break;
504             }
505
506             string_t prop = argv[i];
507             size_t delim_maybe = prop.find(W('='));
508             if (delim_maybe == string_t::npos)
509             {
510                 pal::fprintf(stderr, W("Option %s: '%s' missing property value\n"), arg, prop.c_str());
511                 break;
512             }
513
514             string_t key = prop.substr(0, delim_maybe);
515             string_t value = prop.substr(delim_maybe + 1);
516             config.user_defined_keys.push_back(std::move(key));
517             config.user_defined_values.push_back(std::move(value));
518         }
519         else if (pal::strcmp(option, W("d")) == 0 || (pal::strcmp(option, W("debug")) == 0))
520         {
521             config.wait_to_debug = true;
522         }
523         else if (pal::strcmp(option, W("st")) == 0)
524         {
525             config.self_test = true;
526             return true;
527         }
528         else if ((pal::strcmp(option, W("?")) == 0 || (pal::strcmp(option, W("h")) == 0 || (pal::strcmp(option, W("help")) == 0))))
529         {
530             display_usage();
531             break;
532         }
533         // Now we use corerun to run corefx tests instead of dotnet, because last one isn't available for
534         // Tizen/armel. So we need to skip dotnet specific arguments, we patch corerun for it because
535         // Microsoft.DotNet.RemoteExecutor tries to execute binary that it gets from /proc/self/maps, so we
536         // need a binary that will behave like dotnet.
537         else if (pal::strcmp(option, W("--runtimeconfig")) == 0 || pal::strcmp(option, W("--depsfile")) == 0)
538         {
539             i++;
540             continue;
541         }
542         else
543         {
544             pal::fprintf(stderr, W("Unknown option %s\n"), arg);
545             break;
546         }
547     }
548
549     return false;
550 }
551
552 // Forward declaration for self testing method.
553 static int self_test();
554
555 //
556 // Entry points
557 //
558
559 int MAIN(const int argc, const char_t* argv[])
560 {
561     configuration config{};
562     if (!parse_args(argc, argv, config))
563         return EXIT_FAILURE;
564
565     if (config.self_test)
566         return self_test();
567
568     int exit_code = run(config);
569     return exit_code;
570 }
571
572 #ifdef TARGET_WINDOWS
573 // Used by CoreShim to determine running CoreCLR details.
574 extern "C" __declspec(dllexport) HRESULT __cdecl GetCurrentClrDetails(void** clrInstance, unsigned int* appDomainId)
575 {
576     assert(clrInstance != nullptr && appDomainId != nullptr);
577     *clrInstance = CurrentClrInstance;
578     *appDomainId = CurrentAppDomainId;
579     return S_OK;
580 }
581 #endif // TARGET_WINDOWS
582
583 //
584 // Self testing for corerun.
585 //
586
587 #define THROW_IF_FALSE(stmt) if (!(stmt)) throw W(#stmt);
588 #define THROW_IF_TRUE(stmt) if (stmt) throw W(#stmt);
589 static int self_test()
590 {
591     try
592     {
593         {
594             configuration config{};
595             const char_t* args[] = { W(""), W("-d"), W("foo") };
596             THROW_IF_FALSE(parse_args(3, args, config));
597             THROW_IF_FALSE(config.wait_to_debug);
598             THROW_IF_FALSE(config.clr_path.empty());
599             THROW_IF_FALSE(!config.entry_assembly_fullpath.empty());
600             THROW_IF_FALSE(config.entry_assembly_argc == 0);
601         }
602         {
603             configuration config{};
604             const char_t* args[] = { W(""), W("-d"), W("foo"), W("1"), W("2"), W("3") };
605             THROW_IF_FALSE(parse_args(6, args, config));
606             THROW_IF_FALSE(config.wait_to_debug);
607             THROW_IF_FALSE(!config.entry_assembly_fullpath.empty());
608             THROW_IF_FALSE(config.entry_assembly_argc == 3);
609         }
610         {
611             configuration config{};
612             const char_t* args[] = { W(""), W("--clr-path"), W("path"), W("foo"), W("1") };
613             THROW_IF_FALSE(parse_args(5, args, config));
614             THROW_IF_FALSE(!config.wait_to_debug);
615             THROW_IF_FALSE(config.clr_path == W("path"));
616             THROW_IF_FALSE(!config.entry_assembly_fullpath.empty());
617             THROW_IF_FALSE(config.entry_assembly_argc == 1);
618         }
619         {
620             configuration config{};
621             const char_t* args[] = { W(""), W("-p"), W("invalid"), W("foo") };
622             THROW_IF_TRUE(parse_args(4, args, config));
623         }
624         {
625             configuration config{};
626             const char_t* args[] = { W(""), W("-p"), W("empty="), W("foo") };
627             THROW_IF_FALSE(parse_args(4, args, config));
628             THROW_IF_FALSE(config.user_defined_keys.size() == 1);
629             THROW_IF_FALSE(config.user_defined_values.size() == 1);
630             THROW_IF_FALSE(!config.entry_assembly_fullpath.empty());
631         }
632         {
633             configuration config{};
634             const char_t* args[] = { W(""), W("-p"), W("one=1"), W("foo") };
635             THROW_IF_FALSE(parse_args(4, args, config));
636             THROW_IF_FALSE(config.user_defined_keys.size() == 1);
637             THROW_IF_FALSE(config.user_defined_values.size() == 1);
638             THROW_IF_FALSE(!config.entry_assembly_fullpath.empty());
639         }
640         {
641             configuration config{};
642             const char_t* args[] = { W(""), W("-p"), W("one=1"), W("--property"), W("System.GC.Concurrent=true"), W("foo") };
643             THROW_IF_FALSE(parse_args(6, args, config));
644             THROW_IF_FALSE(config.user_defined_keys.size() == 2);
645             THROW_IF_FALSE(config.user_defined_values.size() == 2);
646             THROW_IF_FALSE(!config.entry_assembly_fullpath.empty());
647         }
648         {
649             string_t path;
650             path = W("path");
651             pal::ensure_trailing_delimiter(path);
652             THROW_IF_FALSE(path.back() == pal::dir_delim);
653             path = W("");
654             pal::ensure_trailing_delimiter(path);
655             THROW_IF_FALSE(path.back() == pal::dir_delim);
656             path = W("\\");
657             pal::ensure_trailing_delimiter(path);
658             THROW_IF_FALSE(path.back() == pal::dir_delim || path.length() == 1);
659             path = W("/");
660             pal::ensure_trailing_delimiter(path);
661             THROW_IF_FALSE(path.back() == pal::dir_delim || path.length() == 1);
662         }
663         {
664             THROW_IF_FALSE(!pal::string_ends_with(W(""), W(".cd")));
665             THROW_IF_FALSE(pal::string_ends_with(W("ab.cd"), W(".cd")));
666             THROW_IF_FALSE(!pal::string_ends_with(W("ab.cd"), W(".cde")));
667             THROW_IF_FALSE(!pal::string_ends_with(W("ab.cd"), W("ab.cde")));
668         }
669     }
670     catch (const char_t msg[])
671     {
672         pal::fprintf(stderr, W("Fail: %s\n"), msg);
673         return EXIT_FAILURE;
674     }
675
676     pal::fprintf(stdout, W("Self-test passed.\n"));
677     return EXIT_SUCCESS;
678 }