]> cygwin.com Git - cygwin-apps/setup.git/blob - win32.cc
cd7fec514bc2539026fb3ee8313789fc4e47f164
[cygwin-apps/setup.git] / win32.cc
1 /*
2 * Copyright (c) 2007 Brian Dessent
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 Brian Dessent <brian@dessent.net>
13 *
14 */
15
16 #include "win32.h"
17 #include <memory>
18 #include <malloc.h>
19 #include "LogFile.h"
20 #include "resource.h"
21 #include "ini.h"
22 #include <sys/stat.h>
23 #include "String++.h"
24
25 NTSecurity nt_sec;
26
27 #define ALL_INHERIT_ACE (CONTAINER_INHERIT_ACE \
28 | OBJECT_INHERIT_ACE \
29 | INHERIT_ONLY_ACE)
30 #define NO_INHERIT_ACE (0)
31
32 PSECURITY_DESCRIPTOR
33 NTSecurity::GetPosixPerms (const char *fname, PSID owner_sid, PSID group_sid,
34 mode_t mode, SECURITY_DESCRIPTOR &out_sd, acl_t &acl)
35 {
36 DWORD u_attribute, g_attribute, o_attribute;
37 DWORD offset = 0;
38
39 /* Initialize out SD */
40 if (!InitializeSecurityDescriptor (&out_sd, SECURITY_DESCRIPTOR_REVISION))
41 Log (LOG_TIMESTAMP) << "InitializeSecurityDescriptor(" << fname
42 << ") failed: " << GetLastError () << endLog;
43 out_sd.Control |= SE_DACL_PROTECTED;
44
45 /* Initialize ACL and fill with almost POSIX-like permissions.
46 Note that the current user always requires write permissions, otherwise
47 creating files in directories with restricted permissions fails. */
48 if (!InitializeAcl (&acl.acl , sizeof acl, ACL_REVISION))
49 Log (LOG_TIMESTAMP) << "InitializeAcl(" << fname << ") failed: "
50 << GetLastError () << endLog;
51 /* USER */
52 /* Default user to current user. */
53 if (!owner_sid)
54 owner_sid = ownerSID.user.User.Sid;
55 u_attribute = STANDARD_RIGHTS_ALL | FILE_GENERIC_READ | FILE_GENERIC_WRITE;
56 if (mode & 0100) // S_IXUSR
57 u_attribute |= FILE_GENERIC_EXECUTE;
58 if ((mode & 0300) == 0300) // S_IWUSR | S_IXUSR
59 u_attribute |= FILE_DELETE_CHILD;
60 if (!AddAccessAllowedAceEx (&acl.acl, ACL_REVISION, NO_INHERIT_ACE,
61 u_attribute, owner_sid))
62 Log (LOG_TIMESTAMP) << "AddAccessAllowedAceEx(" << fname
63 << ", owner) failed: " << GetLastError () << endLog;
64 else
65 offset++;
66 /* GROUP */
67 /* Default group to current primary group. */
68 if (!group_sid)
69 group_sid = groupSID;
70 g_attribute = STANDARD_RIGHTS_READ | FILE_READ_ATTRIBUTES;
71 if (mode & 0040) // S_IRGRP
72 g_attribute |= FILE_GENERIC_READ;
73 if (mode & 0020) // S_IWGRP
74 g_attribute |= FILE_GENERIC_WRITE;
75 if (mode & 0010) // S_IXGRP
76 g_attribute |= FILE_GENERIC_EXECUTE;
77 if ((mode & 01030) == 00030) // S_IWGRP | S_IXGRP, !S_ISVTX
78 g_attribute |= FILE_DELETE_CHILD;
79 if (!AddAccessAllowedAceEx (&acl.acl, ACL_REVISION, NO_INHERIT_ACE,
80 g_attribute, group_sid))
81 Log (LOG_TIMESTAMP) << "AddAccessAllowedAceEx(" << fname
82 << ", group) failed: " << GetLastError () << endLog;
83 else
84 offset++;
85 /* OTHER */
86 o_attribute = STANDARD_RIGHTS_READ | FILE_READ_ATTRIBUTES;
87 if (mode & 0004) // S_IROTH
88 o_attribute |= FILE_GENERIC_READ;
89 if (mode & 0002) // S_IWOTH
90 o_attribute |= FILE_GENERIC_WRITE;
91 if (mode & 0001) // S_IXOTH
92 o_attribute |= FILE_GENERIC_EXECUTE;
93 if ((mode & 01003) == 00003) // S_IWOTH | S_IXOTH, !S_ISVTX
94 o_attribute |= FILE_DELETE_CHILD;
95 if (!AddAccessAllowedAceEx (&acl.acl, ACL_REVISION, NO_INHERIT_ACE,
96 o_attribute, everyOneSID.theSID ()))
97 Log (LOG_TIMESTAMP) << "AddAccessAllowedAceEx(" << fname
98 << ", everyone) failed: " << GetLastError () << endLog;
99 else
100 offset++;
101 if (mode & 07000) /* At least one of S_ISUID, S_ISGID, S_ISVTX */
102 {
103 DWORD attribute = 0;
104 if (mode & 04000) // S_ISUID
105 attribute |= FILE_APPEND_DATA;
106 if (mode & 02000) // S_ISGID
107 attribute |= FILE_WRITE_DATA;
108 if (mode & 01000) // S_ISVTX
109 attribute |= FILE_READ_DATA;
110 if (!AddAccessAllowedAceEx (&acl.acl, ACL_REVISION, NO_INHERIT_ACE,
111 attribute, nullSID.theSID ()))
112 Log (LOG_TIMESTAMP) << "AddAccessAllowedAceEx(" << fname
113 << ", null) failed: " << GetLastError () << endLog;
114 else
115 offset++;
116 }
117 /* For directories, we also add inherit-only ACEs for CREATOR OWNER,
118 CREATOR GROUP, and EVERYONE (aka OTHER). */
119 if (mode & S_IFDIR)
120 {
121 if (mode & 01000) // S_ISVTX
122 {
123 /* Don't allow default write permissions for group and other
124 in a S_ISVTX dir. */
125 /* GROUP */
126 g_attribute = STANDARD_RIGHTS_READ | FILE_READ_ATTRIBUTES;
127 if (mode & 0040) // S_IRGRP
128 g_attribute |= FILE_GENERIC_READ;
129 if (mode & 0010) // S_IXGRP
130 g_attribute |= FILE_GENERIC_EXECUTE;
131 /* OTHER */
132 o_attribute = STANDARD_RIGHTS_READ | FILE_READ_ATTRIBUTES;
133 if (mode & 0004) // S_IROTH
134 o_attribute |= FILE_GENERIC_READ;
135 if (mode & 0001) // S_IXOTH
136 o_attribute |= FILE_GENERIC_EXECUTE;
137 }
138 if (!AddAccessAllowedAceEx (&acl.acl, ACL_REVISION, ALL_INHERIT_ACE,
139 u_attribute, cr_ownerSID.theSID ()))
140 Log (LOG_TIMESTAMP) << "AddAccessAllowedAceEx(" << fname
141 << ", creator owner) failed: "
142 << GetLastError () << endLog;
143 else
144 offset++;
145 if (!AddAccessAllowedAceEx (&acl.acl, ACL_REVISION, ALL_INHERIT_ACE,
146 g_attribute, cr_groupSID.theSID ()))
147 Log (LOG_TIMESTAMP) << "AddAccessAllowedAceEx(" << fname
148 << ", creator group) failed: "
149 << GetLastError () << endLog;
150 else
151 offset++;
152 if (!AddAccessAllowedAceEx (&acl.acl, ACL_REVISION, ALL_INHERIT_ACE,
153 o_attribute, everyOneSID.theSID ()))
154 Log (LOG_TIMESTAMP) << "AddAccessAllowedAceEx(" << fname
155 << ", everyone inherit) failed: "
156 << GetLastError () << endLog;
157 else
158 offset++;
159 }
160
161 /* Set SD's DACL to just created ACL. */
162 if (!SetSecurityDescriptorDacl (&out_sd, TRUE, &acl.acl, FALSE))
163 Log (LOG_TIMESTAMP) << "SetSecurityDescriptorDacl(" << fname
164 << ") failed: " << GetLastError () << endLog;
165 return &out_sd;
166 }
167
168 void
169 NTSecurity::NoteFailedAPI (const std::string &api)
170 {
171 Log (LOG_TIMESTAMP) << api << "() failed: " << GetLastError () << endLog;
172 }
173
174 void
175 NTSecurity::initialiseWellKnownSIDs ()
176 {
177 SID_IDENTIFIER_AUTHORITY n_sid_auth = { SECURITY_NULL_SID_AUTHORITY };
178 /* Get the SID for "NULL" S-1-0-0 */
179 if (!AllocateAndInitializeSid (&n_sid_auth, 1, SECURITY_NULL_RID,
180 0, 0, 0, 0, 0, 0, 0, &nullSID.theSID ()))
181 return;
182 SID_IDENTIFIER_AUTHORITY e_sid_auth = { SECURITY_WORLD_SID_AUTHORITY };
183 /* Get the SID for "Everyone" S-1-1-0 */
184 if (!AllocateAndInitializeSid (&e_sid_auth, 1, SECURITY_WORLD_RID,
185 0, 0, 0, 0, 0, 0, 0, &everyOneSID.theSID ()))
186 return;
187 SID_IDENTIFIER_AUTHORITY nt_sid_auth = { SECURITY_NT_AUTHORITY };
188 /* Get the SID for "Administrators" S-1-5-32-544 */
189 if (!AllocateAndInitializeSid (&nt_sid_auth, 2, SECURITY_BUILTIN_DOMAIN_RID,
190 DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0,
191 &administratorsSID.theSID ()))
192 return;
193 /* Get the SID for "Users" S-1-5-32-545 */
194 if (!AllocateAndInitializeSid (&nt_sid_auth, 2, SECURITY_BUILTIN_DOMAIN_RID,
195 DOMAIN_ALIAS_RID_USERS, 0, 0, 0, 0, 0, 0,
196 &usersSID.theSID ()))
197 return;
198 SID_IDENTIFIER_AUTHORITY c_sid_auth = { SECURITY_CREATOR_SID_AUTHORITY };
199 /* Get the SID for "CREATOR OWNER" S-1-3-0 */
200 if (!AllocateAndInitializeSid (&c_sid_auth, 1, SECURITY_CREATOR_OWNER_RID,
201 0, 0, 0, 0, 0, 0, 0, &cr_ownerSID.theSID ()))
202 return;
203 /* Get the SID for "CREATOR GROUP" S-1-3-1 */
204 if (!AllocateAndInitializeSid (&c_sid_auth, 1, SECURITY_CREATOR_GROUP_RID,
205 0, 0, 0, 0, 0, 0, 0, &cr_groupSID.theSID ()))
206 return;
207 wellKnownSIDsinitialized (true);
208 }
209
210 void
211 NTSecurity::setDefaultDACL ()
212 {
213 /* To assure that the created files have a useful ACL, the
214 default DACL in the process token is set to full access to
215 everyone. This applies to files and subdirectories created
216 in directories which don't propagate permissions to child
217 objects.
218 To assure that the files group is meaningful, a token primary
219 group of None is changed to Users or Administrators.
220 This is the fallback if real POSIX permissions don't
221 work for some reason. */
222
223 /* Create a buffer which has enough room to contain the TOKEN_DEFAULT_DACL
224 structure plus an ACL with one ACE. */
225 size_t bufferSize = sizeof (ACL) + sizeof (ACCESS_ALLOWED_ACE)
226 + GetLengthSid (everyOneSID.theSID ()) - sizeof (DWORD);
227
228 std::unique_ptr<char[]> buf (new char[bufferSize]);
229
230 /* First initialize the TOKEN_DEFAULT_DACL structure. */
231 PACL dacl = (PACL) buf.get ();
232
233 /* Initialize the ACL for containing one ACE. */
234 if (!InitializeAcl (dacl, bufferSize, ACL_REVISION))
235 {
236 NoteFailedAPI ("InitializeAcl");
237 return;
238 }
239
240 /* Create the ACE which grants full access to "Everyone" and store it
241 in dacl. */
242 if (!AddAccessAllowedAceEx (dacl, ACL_REVISION, NO_INHERIT_ACE,
243 GENERIC_ALL, everyOneSID.theSID ()))
244 {
245 NoteFailedAPI ("AddAccessAllowedAceEx");
246 return;
247 }
248
249 /* Set the default DACL to the above computed ACL. */
250 if (!SetTokenInformation (token.theHANDLE(), TokenDefaultDacl, &dacl,
251 bufferSize))
252 NoteFailedAPI ("SetTokenInformation");
253 }
254
255 void
256 NTSecurity::setBackupPrivileges ()
257 {
258 LUID backup, restore;
259 if (!LookupPrivilegeValue (NULL, SE_BACKUP_NAME, &backup))
260 NoteFailedAPI ("LookupPrivilegeValue");
261 else if (!LookupPrivilegeValue (NULL, SE_RESTORE_NAME, &restore))
262 NoteFailedAPI ("LookupPrivilegeValue");
263 else
264 {
265 PTOKEN_PRIVILEGES new_privs;
266
267 new_privs = (PTOKEN_PRIVILEGES) alloca (sizeof (TOKEN_PRIVILEGES)
268 + sizeof (LUID_AND_ATTRIBUTES));
269 new_privs->PrivilegeCount = 2;
270 new_privs->Privileges[0].Luid = backup;
271 new_privs->Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
272 new_privs->Privileges[1].Luid = restore;
273 new_privs->Privileges[1].Attributes = SE_PRIVILEGE_ENABLED;
274 if (!AdjustTokenPrivileges (token.theHANDLE (), FALSE, new_privs,
275 0, NULL, NULL))
276 NoteFailedAPI ("AdjustTokenPrivileges");
277 else if (GetLastError () == ERROR_NOT_ALL_ASSIGNED)
278 Log (LOG_TIMESTAMP) << "User has NO backup/restore rights" << endLog;
279 else
280 Log (LOG_TIMESTAMP) << "User has backup/restore rights" << endLog;
281 }
282 }
283
284 void
285 NTSecurity::resetPrimaryGroup ()
286 {
287 if (primaryGroupSID.pgrp.PrimaryGroup)
288 {
289 Log (LOG_TIMESTAMP) << "Changing gid back to original" << endLog;
290 if (!SetTokenInformation (token.theHANDLE (), TokenPrimaryGroup,
291 &primaryGroupSID, sizeof primaryGroupSID))
292 NoteFailedAPI ("SetTokenInformation");
293 }
294 }
295
296 void
297 NTSecurity::setAdminGroup ()
298 {
299 TOKEN_PRIMARY_GROUP tpg;
300
301 tpg.PrimaryGroup = administratorsSID.theSID ();
302 Log (LOG_TIMESTAMP) << "Changing gid to Administrators" << endLog;
303 if (!SetTokenInformation (token.theHANDLE (), TokenPrimaryGroup,
304 &tpg, sizeof tpg))
305 NoteFailedAPI ("SetTokenInformation");
306 else
307 groupSID = administratorsSID.theSID ();
308 }
309
310 void
311 NTSecurity::setDefaultSecurity ()
312 {
313 /* Get the processes access token. */
314 if (!OpenProcessToken (GetCurrentProcess (),
315 TOKEN_READ | TOKEN_ADJUST_DEFAULT
316 | TOKEN_ADJUST_PRIVILEGES, &token.theHANDLE ()))
317 {
318 NoteFailedAPI ("OpenProcessToken");
319 return;
320 }
321
322 /* Set backup and restore privileges if available. */
323 setBackupPrivileges ();
324
325 /* Log if symlink creation privilege is available. */
326 if (!hasSymlinkCreationRights())
327 Log (LOG_TIMESTAMP) << "User has NO symlink creation right" << endLog;
328 else
329 Log (LOG_TIMESTAMP) << "User has symlink creation right" << endLog;
330
331 /* If initializing the well-known SIDs didn't work, we're finished here. */
332 if (!wellKnownSIDsinitialized ())
333 return;
334
335 /* Set the default DACL to all permissions for everyone as a fallback. */
336 setDefaultDACL ();
337
338 /* Get the user */
339 if (!GetTokenInformation (token.theHANDLE (), TokenUser, &ownerSID,
340 sizeof ownerSID, &size))
341 {
342 NoteFailedAPI ("GetTokenInformation(user)");
343 return;
344 }
345 /* Make it the owner */
346 TOKEN_OWNER owner = { ownerSID.user.User.Sid };
347 if (!SetTokenInformation (token.theHANDLE (), TokenOwner, &owner,
348 sizeof owner))
349 {
350 NoteFailedAPI ("SetTokenInformation(owner)");
351 return;
352 }
353 /* Get original primary group */
354 if (!GetTokenInformation (token.theHANDLE (), TokenPrimaryGroup,
355 &primaryGroupSID, sizeof primaryGroupSID, &size))
356 {
357 NoteFailedAPI("GetTokenInformation(pgrp)");
358 primaryGroupSID.pgrp.PrimaryGroup = (PSID) NULL;
359 }
360 groupSID = primaryGroupSID.pgrp.PrimaryGroup;
361 }
362
363 bool
364 NTSecurity::isRunAsAdmin ()
365 {
366 BOOL is_run_as_admin = FALSE;
367 if (!CheckTokenMembership(NULL, administratorsSID.theSID (), &is_run_as_admin))
368 NoteFailedAPI("CheckTokenMembership(administratorsSID)");
369 return (is_run_as_admin == TRUE);
370 }
371
372 bool
373 NTSecurity::hasSymlinkCreationRights ()
374 {
375 LUID symlink;
376 if (!LookupPrivilegeValue (NULL, SE_CREATE_SYMBOLIC_LINK_NAME, &symlink))
377 {
378 NoteFailedAPI ("LookupPrivilegeValue");
379 return FALSE;
380 }
381
382 DWORD size;
383 GetTokenInformation (token.theHANDLE (), TokenPrivileges, NULL, 0, &size);
384 /* Will fail ERROR_INSUFFICIENT_BUFFER, but updates size */
385
386 TOKEN_PRIVILEGES *privileges = (TOKEN_PRIVILEGES *)alloca(size);
387 if (!GetTokenInformation (token.theHANDLE (), TokenPrivileges, privileges,
388 size, &size))
389 {
390 NoteFailedAPI ("GetTokenInformation(privileges)");
391 return FALSE;
392 }
393
394 unsigned int i;
395 for (i = 0; i < privileges->PrivilegeCount; i++)
396 {
397 if (memcmp(&privileges->Privileges[i].Luid, &symlink, sizeof(LUID)) == 0)
398 {
399 return TRUE;
400 }
401 }
402
403 return FALSE;
404 }
405
406 VersionInfo::VersionInfo ()
407 {
408 v.dwOSVersionInfoSize = sizeof (OSVERSIONINFO);
409 if (GetVersionEx (&v) == 0)
410 {
411 Log (LOG_PLAIN) << "GetVersionEx () failed: " << GetLastError ()
412 << endLog;
413
414 /* If GetVersionEx fails we really should bail with an error of some kind,
415 but for now just assume we're on NT and continue. */
416 v.dwPlatformId = VER_PLATFORM_WIN32_NT;
417 }
418 }
419
420 /* This is the Construct on First Use idiom to avoid static initialization
421 order problems. */
422 VersionInfo& GetVer ()
423 {
424 static VersionInfo *vi = new VersionInfo ();
425 return *vi;
426 }
427
428 /* Identify native machine arch if we are running under WoW */
429 USHORT
430 WowNativeMachine ()
431 {
432 typedef BOOL (WINAPI *PFNISWOW64PROCESS2)(HANDLE, USHORT *, USHORT *);
433 PFNISWOW64PROCESS2 pfnIsWow64Process2 = (PFNISWOW64PROCESS2)GetProcAddress(GetModuleHandle("kernel32"), "IsWow64Process2");
434
435 typedef BOOL (WINAPI *PFNISWOW64PROCESS)(HANDLE, PBOOL);
436 PFNISWOW64PROCESS pfnIsWow64Process = (PFNISWOW64PROCESS)GetProcAddress(GetModuleHandle("kernel32"), "IsWow64Process");
437
438 USHORT processMachine, nativeMachine;
439 if ((pfnIsWow64Process2) &&
440 (pfnIsWow64Process2(GetCurrentProcess(), &processMachine, &nativeMachine)))
441 return nativeMachine;
442 else if (pfnIsWow64Process) {
443 #ifdef _X86_
444 BOOL bIsWow64 = FALSE;
445 if (pfnIsWow64Process(GetCurrentProcess(), &bIsWow64))
446 return bIsWow64 ? IMAGE_FILE_MACHINE_AMD64 : IMAGE_FILE_MACHINE_I386;
447 #endif
448 }
449
450 #ifdef __x86_64__
451 return IMAGE_FILE_MACHINE_AMD64;
452 #else
453 return IMAGE_FILE_MACHINE_I386;
454 #endif
455 }
456
457 /* Identify machine arch for the current process */
458 USHORT
459 WindowsProcessMachine ()
460 {
461 #if defined(__x86_64__)
462 USHORT processMachine = IMAGE_FILE_MACHINE_AMD64;
463 #elif defined(__i386__)
464 USHORT processMachine = IMAGE_FILE_MACHINE_I386;
465 #elif defined(__aarch64__)
466 USHORT processMachine = IMAGE_FILE_MACHINE_ARM64;
467 #else
468 #error "Unknown architecture"
469 #endif
470 return processMachine;
471 }
472
473 /* Convert a machine arch identifier to a string */
474 const std::string
475 machine_name(USHORT machine)
476 {
477 switch (machine)
478 {
479 case IMAGE_FILE_MACHINE_I386:
480 return "x86";
481 break;
482 case IMAGE_FILE_MACHINE_AMD64:
483 return "x86_64";
484 break;
485 case IMAGE_FILE_MACHINE_ARM64:
486 return "ARM64";
487 break;
488 default:
489 std::stringstream machine_desc;
490 machine_desc << std::hex << machine;
491 return machine_desc.str();
492 }
493 }
494
495 const std::wstring
496 LoadStringWEx(UINT uID, UINT langId)
497 {
498 HINSTANCE hInstance = GetModuleHandle(NULL);
499
500 // Convert the string ID into a bundle number
501 LPCSTR bundle = MAKEINTRESOURCE(uID / 16 + 1);
502 HRSRC hRes = ::FindResourceEx(hInstance, RT_STRING, bundle, langId);
503 if (hRes)
504 {
505 HGLOBAL h = ::LoadResource(hInstance, hRes);
506 if (h)
507 {
508 HGLOBAL hGlob = ::LockResource(h);
509
510 // walk string bundle
511 wchar_t *buf = (wchar_t *)hGlob;
512 for (unsigned int i = 0; i < (uID & 15); i++)
513 {
514 buf += 1 + (UINT)*buf;
515 }
516
517 int len = *buf;
518 return std::wstring(buf + 1, len);
519 }
520 }
521 // N.B.: Due to the way string bundles are encoded, there's no difference
522 // between an absent string resource whose bundle is present, and a string
523 // resource containing the null string.
524 return L"";
525 }
526
527 const std::wstring
528 LoadStringW(unsigned int uID)
529 {
530 wchar_t *buf;
531
532 int len = ::LoadStringW(GetModuleHandle(NULL), uID, (LPWSTR)&buf, 0);
533 if (len > 0)
534 return std::wstring(buf, len);
535
536 // if empty or absent, fallback to the untranslated string
537 return LoadStringWEx(uID, MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US));
538 }
539
540 const std::string
541 LoadStringUtf8(unsigned int uID)
542 {
543 return wstring_to_string(LoadStringW(uID));
544 }
545
546 bool
547 is_developer_mode(void)
548 {
549 HKEY hKey;
550 LSTATUS err = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock", 0, KEY_READ, &hKey);
551 if (err != ERROR_SUCCESS)
552 return false;
553
554 DWORD value;
555 DWORD size = sizeof(DWORD);
556 err = RegQueryValueExW(hKey, L"AllowDevelopmentWithoutDevLicense", NULL, NULL, reinterpret_cast<LPBYTE>(&value), &size);
557 RegCloseKey(hKey);
558 if (err != ERROR_SUCCESS)
559 return false;
560
561 return value != 0;
562 }
This page took 0.065446 seconds and 6 git commands to generate.