1 /* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
2    file Copyright.txt or https://cmake.org/licensing for details.  */
3 #include "cmCPackDragNDropGenerator.h"
4 
5 #include <algorithm>
6 #include <cstdlib>
7 #include <iomanip>
8 #include <map>
9 
10 #include <CoreFoundation/CoreFoundation.h>
11 #include <cm3p/kwiml/abi.h>
12 
13 #include "cmsys/Base64.h"
14 #include "cmsys/FStream.hxx"
15 #include "cmsys/RegularExpression.hxx"
16 
17 #include "cmCPackGenerator.h"
18 #include "cmCPackLog.h"
19 #include "cmDuration.h"
20 #include "cmGeneratedFileStream.h"
21 #include "cmStringAlgorithms.h"
22 #include "cmSystemTools.h"
23 #include "cmValue.h"
24 #include "cmXMLWriter.h"
25 
26 #ifdef HAVE_CoreServices
27 // For the old LocaleStringToLangAndRegionCodes() function, to convert
28 // to the old Script Manager RegionCode values needed for the 'LPic' data
29 // structure used for generating multi-lingual SLAs.
30 #  include <CoreServices/CoreServices.h>
31 #endif
32 
33 static const uint16_t DefaultLpic[] = {
34   /* clang-format off */
35   0x0002, 0x0011, 0x0003, 0x0001, 0x0000, 0x0000, 0x0002, 0x0000,
36   0x0008, 0x0003, 0x0000, 0x0001, 0x0004, 0x0000, 0x0004, 0x0005,
37   0x0000, 0x000E, 0x0006, 0x0001, 0x0005, 0x0007, 0x0000, 0x0007,
38   0x0008, 0x0000, 0x0047, 0x0009, 0x0000, 0x0034, 0x000A, 0x0001,
39   0x0035, 0x000B, 0x0001, 0x0020, 0x000C, 0x0000, 0x0011, 0x000D,
40   0x0000, 0x005B, 0x0004, 0x0000, 0x0033, 0x000F, 0x0001, 0x000C,
41   0x0010, 0x0000, 0x000B, 0x000E, 0x0000
42   /* clang-format on */
43 };
44 
45 static const std::vector<std::string> DefaultMenu = {
46   { "English", "Agree", "Disagree", "Print", "Save...",
47     // NOLINTNEXTLINE(bugprone-suspicious-missing-comma)
48     "You agree to the License Agreement terms when "
49     "you click the \"Agree\" button.",
50     "Software License Agreement",
51     "This text cannot be saved.  "
52     "This disk may be full or locked, or the file may be locked.",
53     "Unable to print.  Make sure you have selected a printer." }
54 };
55 
cmCPackDragNDropGenerator()56 cmCPackDragNDropGenerator::cmCPackDragNDropGenerator()
57   : singleLicense(false)
58 {
59   // default to one package file for components
60   this->componentPackageMethod = ONE_PACKAGE;
61 }
62 
63 cmCPackDragNDropGenerator::~cmCPackDragNDropGenerator() = default;
64 
InitializeInternal()65 int cmCPackDragNDropGenerator::InitializeInternal()
66 {
67   // Starting with Xcode 4.3, look in "/Applications/Xcode.app" first:
68   //
69   std::vector<std::string> paths;
70   paths.emplace_back("/Applications/Xcode.app/Contents/Developer/Tools");
71   paths.emplace_back("/Developer/Tools");
72 
73   const std::string hdiutil_path =
74     cmSystemTools::FindProgram("hdiutil", std::vector<std::string>(), false);
75   if (hdiutil_path.empty()) {
76     cmCPackLogger(cmCPackLog::LOG_ERROR,
77                   "Cannot locate hdiutil command" << std::endl);
78     return 0;
79   }
80   this->SetOptionIfNotSet("CPACK_COMMAND_HDIUTIL", hdiutil_path);
81 
82   const std::string setfile_path =
83     cmSystemTools::FindProgram("SetFile", paths, false);
84   if (setfile_path.empty()) {
85     cmCPackLogger(cmCPackLog::LOG_ERROR,
86                   "Cannot locate SetFile command" << std::endl);
87     return 0;
88   }
89   this->SetOptionIfNotSet("CPACK_COMMAND_SETFILE", setfile_path);
90 
91   const std::string rez_path = cmSystemTools::FindProgram("Rez", paths, false);
92   if (rez_path.empty()) {
93     cmCPackLogger(cmCPackLog::LOG_ERROR,
94                   "Cannot locate Rez command" << std::endl);
95     return 0;
96   }
97   this->SetOptionIfNotSet("CPACK_COMMAND_REZ", rez_path);
98 
99   if (this->IsSet("CPACK_DMG_SLA_DIR")) {
100     slaDirectory = this->GetOption("CPACK_DMG_SLA_DIR");
101     if (!slaDirectory.empty() && this->IsSet("CPACK_RESOURCE_FILE_LICENSE")) {
102       std::string license_file =
103         this->GetOption("CPACK_RESOURCE_FILE_LICENSE");
104       if (!license_file.empty() &&
105           (license_file.find("CPack.GenericLicense.txt") ==
106            std::string::npos)) {
107         cmCPackLogger(
108           cmCPackLog::LOG_OUTPUT,
109           "Both CPACK_DMG_SLA_DIR and CPACK_RESOURCE_FILE_LICENSE specified, "
110           "using CPACK_RESOURCE_FILE_LICENSE as a license for all languages."
111             << std::endl);
112         singleLicense = true;
113       }
114     }
115     if (!this->IsSet("CPACK_DMG_SLA_LANGUAGES")) {
116       cmCPackLogger(cmCPackLog::LOG_ERROR,
117                     "CPACK_DMG_SLA_DIR set but no languages defined "
118                     "(set CPACK_DMG_SLA_LANGUAGES)"
119                       << std::endl);
120       return 0;
121     }
122     if (!cmSystemTools::FileExists(slaDirectory, false)) {
123       cmCPackLogger(cmCPackLog::LOG_ERROR,
124                     "CPACK_DMG_SLA_DIR does not exist" << std::endl);
125       return 0;
126     }
127 
128     std::vector<std::string> languages =
129       cmExpandedList(this->GetOption("CPACK_DMG_SLA_LANGUAGES"));
130     if (languages.empty()) {
131       cmCPackLogger(cmCPackLog::LOG_ERROR,
132                     "CPACK_DMG_SLA_LANGUAGES set but empty" << std::endl);
133       return 0;
134     }
135     for (auto const& language : languages) {
136       std::string license = slaDirectory + "/" + language + ".license.txt";
137       std::string license_rtf = slaDirectory + "/" + language + ".license.rtf";
138       if (!singleLicense) {
139         if (!cmSystemTools::FileExists(license) &&
140             !cmSystemTools::FileExists(license_rtf)) {
141           cmCPackLogger(cmCPackLog::LOG_ERROR,
142                         "Missing license file "
143                           << language << ".license.txt"
144                           << " / " << language << ".license.rtf" << std::endl);
145           return 0;
146         }
147       }
148       std::string menu = slaDirectory + "/" + language + ".menu.txt";
149       if (!cmSystemTools::FileExists(menu)) {
150         cmCPackLogger(cmCPackLog::LOG_ERROR,
151                       "Missing menu file " << language << ".menu.txt"
152                                            << std::endl);
153         return 0;
154       }
155     }
156   }
157 
158   return this->Superclass::InitializeInternal();
159 }
160 
GetOutputExtension()161 const char* cmCPackDragNDropGenerator::GetOutputExtension()
162 {
163   return ".dmg";
164 }
165 
PackageFiles()166 int cmCPackDragNDropGenerator::PackageFiles()
167 {
168   // gather which directories to make dmg files for
169   // multiple directories occur if packaging components or groups separately
170 
171   // monolith
172   if (this->Components.empty()) {
173     return this->CreateDMG(toplevel, packageFileNames[0]);
174   }
175 
176   // component install
177   std::vector<std::string> package_files;
178 
179   std::map<std::string, cmCPackComponent>::iterator compIt;
180   for (compIt = this->Components.begin(); compIt != this->Components.end();
181        ++compIt) {
182     std::string name = GetComponentInstallDirNameSuffix(compIt->first);
183     package_files.push_back(name);
184   }
185   std::sort(package_files.begin(), package_files.end());
186   package_files.erase(std::unique(package_files.begin(), package_files.end()),
187                       package_files.end());
188 
189   // loop to create dmg files
190   packageFileNames.clear();
191   for (auto const& package_file : package_files) {
192     std::string full_package_name = std::string(toplevel) + std::string("/");
193     if (package_file == "ALL_IN_ONE") {
194       full_package_name += this->GetOption("CPACK_PACKAGE_FILE_NAME");
195     } else {
196       full_package_name += package_file;
197     }
198     full_package_name += std::string(GetOutputExtension());
199     packageFileNames.push_back(full_package_name);
200 
201     std::string src_dir = cmStrCat(toplevel, '/', package_file);
202 
203     if (0 == this->CreateDMG(src_dir, full_package_name)) {
204       return 0;
205     }
206   }
207   return 1;
208 }
209 
CopyFile(std::ostringstream & source,std::ostringstream & target)210 bool cmCPackDragNDropGenerator::CopyFile(std::ostringstream& source,
211                                          std::ostringstream& target)
212 {
213   if (!cmSystemTools::CopyFileIfDifferent(source.str(), target.str())) {
214     cmCPackLogger(cmCPackLog::LOG_ERROR,
215                   "Error copying " << source.str() << " to " << target.str()
216                                    << std::endl);
217 
218     return false;
219   }
220 
221   return true;
222 }
223 
CreateEmptyFile(std::ostringstream & target,size_t size)224 bool cmCPackDragNDropGenerator::CreateEmptyFile(std::ostringstream& target,
225                                                 size_t size)
226 {
227   cmsys::ofstream fout(target.str().c_str(), std::ios::out | std::ios::binary);
228   if (!fout) {
229     return false;
230   }
231 
232   // Seek to desired size - 1 byte
233   fout.seekp(size - 1, std::ios::beg);
234   char byte = 0;
235   // Write one byte to ensure file grows
236   fout.write(&byte, 1);
237 
238   return true;
239 }
240 
RunCommand(std::ostringstream & command,std::string * output)241 bool cmCPackDragNDropGenerator::RunCommand(std::ostringstream& command,
242                                            std::string* output)
243 {
244   int exit_code = 1;
245 
246   bool result = cmSystemTools::RunSingleCommand(
247     command.str(), output, output, &exit_code, nullptr, this->GeneratorVerbose,
248     cmDuration::zero());
249 
250   if (!result || exit_code) {
251     cmCPackLogger(cmCPackLog::LOG_ERROR,
252                   "Error executing: " << command.str() << std::endl);
253 
254     return false;
255   }
256 
257   return true;
258 }
259 
CreateDMG(const std::string & src_dir,const std::string & output_file)260 int cmCPackDragNDropGenerator::CreateDMG(const std::string& src_dir,
261                                          const std::string& output_file)
262 {
263   // Get optional arguments ...
264   cmValue cpack_package_icon = this->GetOption("CPACK_PACKAGE_ICON");
265 
266   const std::string cpack_dmg_volume_name =
267     this->GetOption("CPACK_DMG_VOLUME_NAME")
268     ? *this->GetOption("CPACK_DMG_VOLUME_NAME")
269     : *this->GetOption("CPACK_PACKAGE_FILE_NAME");
270 
271   const std::string cpack_dmg_format = this->GetOption("CPACK_DMG_FORMAT")
272     ? *this->GetOption("CPACK_DMG_FORMAT")
273     : "UDZO";
274 
275   const std::string cpack_dmg_filesystem =
276     this->GetOption("CPACK_DMG_FILESYSTEM")
277     ? *this->GetOption("CPACK_DMG_FILESYSTEM")
278     : "HFS+";
279 
280   // Get optional arguments ...
281   std::string cpack_license_file =
282     *this->GetOption("CPACK_RESOURCE_FILE_LICENSE");
283 
284   cmValue cpack_dmg_background_image =
285     this->GetOption("CPACK_DMG_BACKGROUND_IMAGE");
286 
287   cmValue cpack_dmg_ds_store = this->GetOption("CPACK_DMG_DS_STORE");
288 
289   cmValue cpack_dmg_languages = this->GetOption("CPACK_DMG_SLA_LANGUAGES");
290 
291   cmValue cpack_dmg_ds_store_setup_script =
292     this->GetOption("CPACK_DMG_DS_STORE_SETUP_SCRIPT");
293 
294   const bool cpack_dmg_disable_applications_symlink =
295     this->IsOn("CPACK_DMG_DISABLE_APPLICATIONS_SYMLINK");
296 
297   // only put license on dmg if is user provided
298   if (!cpack_license_file.empty() &&
299       cpack_license_file.find("CPack.GenericLicense.txt") !=
300         std::string::npos) {
301     cpack_license_file = "";
302   }
303 
304   // use sla_dir if both sla_dir and license_file are set
305   if (!cpack_license_file.empty() && !slaDirectory.empty() && !singleLicense) {
306     cpack_license_file = "";
307   }
308 
309   // The staging directory contains everything that will end-up inside the
310   // final disk image ...
311   std::ostringstream staging;
312   staging << src_dir;
313 
314   // Add a symlink to /Applications so users can drag-and-drop the bundle
315   // into it unless this behavior was disabled
316   if (!cpack_dmg_disable_applications_symlink) {
317     std::ostringstream application_link;
318     application_link << staging.str() << "/Applications";
319     cmSystemTools::CreateSymlink("/Applications", application_link.str());
320   }
321 
322   // Optionally add a custom volume icon ...
323   if (!cpack_package_icon->empty()) {
324     std::ostringstream package_icon_source;
325     package_icon_source << cpack_package_icon;
326 
327     std::ostringstream package_icon_destination;
328     package_icon_destination << staging.str() << "/.VolumeIcon.icns";
329 
330     if (!this->CopyFile(package_icon_source, package_icon_destination)) {
331       cmCPackLogger(cmCPackLog::LOG_ERROR,
332                     "Error copying disk volume icon.  "
333                     "Check the value of CPACK_PACKAGE_ICON."
334                       << std::endl);
335 
336       return 0;
337     }
338   }
339 
340   // Optionally add a custom .DS_Store file
341   // (e.g. for setting background/layout) ...
342   if (!cpack_dmg_ds_store->empty()) {
343     std::ostringstream package_settings_source;
344     package_settings_source << cpack_dmg_ds_store;
345 
346     std::ostringstream package_settings_destination;
347     package_settings_destination << staging.str() << "/.DS_Store";
348 
349     if (!this->CopyFile(package_settings_source,
350                         package_settings_destination)) {
351       cmCPackLogger(cmCPackLog::LOG_ERROR,
352                     "Error copying disk volume settings file.  "
353                     "Check the value of CPACK_DMG_DS_STORE."
354                       << std::endl);
355 
356       return 0;
357     }
358   }
359 
360   // Optionally add a custom background image ...
361   // Make sure the background file type is the same as the custom image
362   // and that the file is hidden so it doesn't show up.
363   if (!cpack_dmg_background_image->empty()) {
364     const std::string extension =
365       cmSystemTools::GetFilenameLastExtension(cpack_dmg_background_image);
366     std::ostringstream package_background_source;
367     package_background_source << cpack_dmg_background_image;
368 
369     std::ostringstream package_background_destination;
370     package_background_destination << staging.str()
371                                    << "/.background/background" << extension;
372 
373     if (!this->CopyFile(package_background_source,
374                         package_background_destination)) {
375       cmCPackLogger(cmCPackLog::LOG_ERROR,
376                     "Error copying disk volume background image.  "
377                     "Check the value of CPACK_DMG_BACKGROUND_IMAGE."
378                       << std::endl);
379 
380       return 0;
381     }
382   }
383 
384   bool remount_image =
385     !cpack_package_icon->empty() || !cpack_dmg_ds_store_setup_script->empty();
386 
387   std::string temp_image_format = "UDZO";
388 
389   // Create 1 MB dummy padding file in staging area when we need to remount
390   // image, so we have enough space for storing changes ...
391   if (remount_image) {
392     std::ostringstream dummy_padding;
393     dummy_padding << staging.str() << "/.dummy-padding-file";
394     if (!this->CreateEmptyFile(dummy_padding, 1048576)) {
395       cmCPackLogger(cmCPackLog::LOG_ERROR,
396                     "Error creating dummy padding file." << std::endl);
397 
398       return 0;
399     }
400     temp_image_format = "UDRW";
401   }
402 
403   // Create a temporary read-write disk image ...
404   std::string temp_image =
405     cmStrCat(this->GetOption("CPACK_TOPLEVEL_DIRECTORY"), "/temp.dmg");
406 
407   std::string create_error;
408   std::ostringstream temp_image_command;
409   temp_image_command << this->GetOption("CPACK_COMMAND_HDIUTIL");
410   temp_image_command << " create";
411   temp_image_command << " -ov";
412   temp_image_command << " -srcfolder \"" << staging.str() << "\"";
413   temp_image_command << " -volname \"" << cpack_dmg_volume_name << "\"";
414   temp_image_command << " -fs \"" << cpack_dmg_filesystem << "\"";
415   temp_image_command << " -format " << temp_image_format;
416   temp_image_command << " \"" << temp_image << "\"";
417 
418   if (!this->RunCommand(temp_image_command, &create_error)) {
419     cmCPackLogger(cmCPackLog::LOG_ERROR,
420                   "Error generating temporary disk image." << std::endl
421                                                            << create_error
422                                                            << std::endl);
423 
424     return 0;
425   }
426 
427   if (remount_image) {
428     // Store that we have a failure so that we always unmount the image
429     // before we exit.
430     bool had_error = false;
431 
432     std::ostringstream attach_command;
433     attach_command << this->GetOption("CPACK_COMMAND_HDIUTIL");
434     attach_command << " attach";
435     attach_command << " \"" << temp_image << "\"";
436 
437     std::string attach_output;
438     if (!this->RunCommand(attach_command, &attach_output)) {
439       cmCPackLogger(cmCPackLog::LOG_ERROR,
440                     "Error attaching temporary disk image." << std::endl);
441 
442       return 0;
443     }
444 
445     cmsys::RegularExpression mountpoint_regex(".*(/Volumes/[^\n]+)\n.*");
446     mountpoint_regex.find(attach_output.c_str());
447     std::string const temp_mount = mountpoint_regex.match(1);
448     std::string const temp_mount_name =
449       temp_mount.substr(sizeof("/Volumes/") - 1);
450 
451     // Remove dummy padding file so we have enough space on RW image ...
452     std::ostringstream dummy_padding;
453     dummy_padding << temp_mount << "/.dummy-padding-file";
454     if (!cmSystemTools::RemoveFile(dummy_padding.str())) {
455       cmCPackLogger(cmCPackLog::LOG_ERROR,
456                     "Error removing dummy padding file." << std::endl);
457 
458       had_error = true;
459     }
460 
461     // Optionally set the custom icon flag for the image ...
462     if (!had_error && !cpack_package_icon->empty()) {
463       std::string error;
464       std::ostringstream setfile_command;
465       setfile_command << this->GetOption("CPACK_COMMAND_SETFILE");
466       setfile_command << " -a C";
467       setfile_command << " \"" << temp_mount << "\"";
468 
469       if (!this->RunCommand(setfile_command, &error)) {
470         cmCPackLogger(cmCPackLog::LOG_ERROR,
471                       "Error assigning custom icon to temporary disk image."
472                         << std::endl
473                         << error << std::endl);
474 
475         had_error = true;
476       }
477     }
478 
479     // Optionally we can execute a custom apple script to generate
480     // the .DS_Store for the volume folder ...
481     if (!had_error && !cpack_dmg_ds_store_setup_script->empty()) {
482       std::ostringstream setup_script_command;
483       setup_script_command << "osascript"
484                            << " \"" << cpack_dmg_ds_store_setup_script << "\""
485                            << " \"" << temp_mount_name << "\"";
486       std::string error;
487       if (!this->RunCommand(setup_script_command, &error)) {
488         cmCPackLogger(cmCPackLog::LOG_ERROR,
489                       "Error executing custom script on disk image."
490                         << std::endl
491                         << error << std::endl);
492 
493         had_error = true;
494       }
495     }
496 
497     std::ostringstream detach_command;
498     detach_command << this->GetOption("CPACK_COMMAND_HDIUTIL");
499     detach_command << " detach";
500     detach_command << " \"" << temp_mount << "\"";
501 
502     if (!this->RunCommand(detach_command)) {
503       cmCPackLogger(cmCPackLog::LOG_ERROR,
504                     "Error detaching temporary disk image." << std::endl);
505 
506       return 0;
507     }
508 
509     if (had_error) {
510       return 0;
511     }
512   }
513 
514   // Create the final compressed read-only disk image ...
515   std::ostringstream final_image_command;
516   final_image_command << this->GetOption("CPACK_COMMAND_HDIUTIL");
517   final_image_command << " convert \"" << temp_image << "\"";
518   final_image_command << " -format ";
519   final_image_command << cpack_dmg_format;
520   final_image_command << " -imagekey";
521   final_image_command << " zlib-level=9";
522   final_image_command << " -o \"" << output_file << "\"";
523 
524   std::string convert_error;
525 
526   if (!this->RunCommand(final_image_command, &convert_error)) {
527     cmCPackLogger(cmCPackLog::LOG_ERROR,
528                   "Error compressing disk image." << std::endl
529                                                   << convert_error
530                                                   << std::endl);
531 
532     return 0;
533   }
534 
535   if (!cpack_license_file.empty() || !slaDirectory.empty()) {
536     // Use old hardcoded style if sla_dir is not set
537     bool oldStyle = slaDirectory.empty();
538     std::string sla_xml =
539       cmStrCat(this->GetOption("CPACK_TOPLEVEL_DIRECTORY"), "/sla.xml");
540 
541     std::vector<std::string> languages;
542     if (!oldStyle) {
543       cmExpandList(cpack_dmg_languages, languages);
544     }
545 
546     std::vector<uint16_t> header_data;
547     if (oldStyle) {
548       header_data = std::vector<uint16_t>(
549         DefaultLpic,
550         DefaultLpic + (sizeof(DefaultLpic) / sizeof(*DefaultLpic)));
551     } else {
552       /*
553        * LPic Layout
554        * (https://github.com/pypt/dmg-add-license/blob/master/main.c)
555        * as far as I can tell (no official documentation seems to exist):
556        * struct LPic {
557        *  uint16_t default_language; // points to a resid, defaulting to 0,
558        *                             // which is the first set language
559        *  uint16_t length;
560        *  struct {
561        *    uint16_t language_code;
562        *    uint16_t resid;
563        *    uint16_t encoding; // Encoding from TextCommon.h,
564        *                       // forcing MacRoman (0) for now. Might need to
565        *                       // allow overwrite per license by user later
566        *  } item[1];
567        * }
568        */
569 
570       header_data.push_back(0);
571       header_data.push_back(languages.size());
572       for (size_t i = 0; i < languages.size(); ++i) {
573         CFStringRef language_cfstring = CFStringCreateWithCString(
574           nullptr, languages[i].c_str(), kCFStringEncodingUTF8);
575         CFStringRef iso_language =
576           CFLocaleCreateCanonicalLanguageIdentifierFromString(
577             nullptr, language_cfstring);
578         if (!iso_language) {
579           cmCPackLogger(cmCPackLog::LOG_ERROR,
580                         languages[i] << " is not a recognized language"
581                                      << std::endl);
582         }
583         char iso_language_cstr[65];
584         CFStringGetCString(iso_language, iso_language_cstr,
585                            sizeof(iso_language_cstr) - 1,
586                            kCFStringEncodingMacRoman);
587         LangCode lang = 0;
588         RegionCode region = 0;
589 #ifdef HAVE_CoreServices
590         OSStatus err =
591           LocaleStringToLangAndRegionCodes(iso_language_cstr, &lang, &region);
592         if (err != noErr)
593 #endif
594         {
595           cmCPackLogger(cmCPackLog::LOG_ERROR,
596                         "No language/region code available for "
597                           << iso_language_cstr << std::endl);
598           return 0;
599         }
600 #ifdef HAVE_CoreServices
601         header_data.push_back(region);
602         header_data.push_back(i);
603         header_data.push_back(0);
604 #endif
605       }
606     }
607 
608     RezDoc rez;
609 
610     {
611       RezDict lpic = { {}, 5000, {} };
612       lpic.Data.reserve(header_data.size() * sizeof(header_data[0]));
613       for (uint16_t x : header_data) {
614         // LPic header is big-endian.
615         char* d = reinterpret_cast<char*>(&x);
616 #if KWIML_ABI_ENDIAN_ID == KWIML_ABI_ENDIAN_ID_LITTLE
617         lpic.Data.push_back(d[1]);
618         lpic.Data.push_back(d[0]);
619 #else
620         lpic.Data.push_back(d[0]);
621         lpic.Data.push_back(d[1]);
622 #endif
623       }
624       rez.LPic.Entries.emplace_back(std::move(lpic));
625     }
626 
627     bool have_write_license_error = false;
628     std::string error;
629 
630     if (oldStyle) {
631       if (!this->WriteLicense(rez, 0, "", cpack_license_file, &error)) {
632         have_write_license_error = true;
633       }
634     } else {
635       for (size_t i = 0; i < languages.size() && !have_write_license_error;
636            ++i) {
637         if (singleLicense) {
638           if (!this->WriteLicense(rez, i + 5000, languages[i],
639                                   cpack_license_file, &error)) {
640             have_write_license_error = true;
641           }
642         } else {
643           if (!this->WriteLicense(rez, i + 5000, languages[i], "", &error)) {
644             have_write_license_error = true;
645           }
646         }
647       }
648     }
649 
650     if (have_write_license_error) {
651       cmCPackLogger(cmCPackLog::LOG_ERROR,
652                     "Error writing license file to SLA." << std::endl
653                                                          << error
654                                                          << std::endl);
655       return 0;
656     }
657 
658     this->WriteRezXML(sla_xml, rez);
659 
660     // Create the final compressed read-only disk image ...
661     std::ostringstream embed_sla_command;
662     embed_sla_command << this->GetOption("CPACK_COMMAND_HDIUTIL");
663     embed_sla_command << " udifrez";
664     embed_sla_command << " -xml";
665     embed_sla_command << " \"" << sla_xml << "\"";
666     embed_sla_command << " FIXME_WHY_IS_THIS_ARGUMENT_NEEDED";
667     embed_sla_command << " \"" << output_file << "\"";
668     std::string embed_error;
669     if (!this->RunCommand(embed_sla_command, &embed_error)) {
670       cmCPackLogger(cmCPackLog::LOG_ERROR,
671                     "Error compressing disk image." << std::endl
672                                                     << embed_error
673                                                     << std::endl);
674 
675       return 0;
676     }
677   }
678 
679   return 1;
680 }
681 
SupportsComponentInstallation() const682 bool cmCPackDragNDropGenerator::SupportsComponentInstallation() const
683 {
684   return true;
685 }
686 
GetComponentInstallDirNameSuffix(const std::string & componentName)687 std::string cmCPackDragNDropGenerator::GetComponentInstallDirNameSuffix(
688   const std::string& componentName)
689 {
690   // we want to group components together that go in the same dmg package
691   std::string package_file_name = this->GetOption("CPACK_PACKAGE_FILE_NAME");
692 
693   // we have 3 mutually exclusive modes to work in
694   // 1. all components in one package
695   // 2. each group goes in its own package with left over
696   //    components in their own package
697   // 3. ignore groups - if grouping is defined, it is ignored
698   //    and each component goes in its own package
699 
700   if (this->componentPackageMethod == ONE_PACKAGE) {
701     return "ALL_IN_ONE";
702   }
703 
704   if (this->componentPackageMethod == ONE_PACKAGE_PER_GROUP) {
705     // We have to find the name of the COMPONENT GROUP
706     // the current COMPONENT belongs to.
707     std::string groupVar =
708       "CPACK_COMPONENT_" + cmSystemTools::UpperCase(componentName) + "_GROUP";
709     cmValue _groupName = this->GetOption(groupVar);
710     if (_groupName) {
711       std::string groupName = _groupName;
712 
713       groupName =
714         GetComponentPackageFileName(package_file_name, groupName, true);
715       return groupName;
716     }
717   }
718 
719   std::string componentFileName =
720     "CPACK_DMG_" + cmSystemTools::UpperCase(componentName) + "_FILE_NAME";
721   if (this->IsSet(componentFileName)) {
722     return this->GetOption(componentFileName);
723   }
724   return GetComponentPackageFileName(package_file_name, componentName, false);
725 }
726 
WriteRezXML(std::string const & file,RezDoc const & rez)727 void cmCPackDragNDropGenerator::WriteRezXML(std::string const& file,
728                                             RezDoc const& rez)
729 {
730   cmGeneratedFileStream fxml(file);
731   cmXMLWriter xml(fxml);
732   xml.StartDocument();
733   xml.StartElement("plist");
734   xml.Attribute("version", "1.0");
735   xml.StartElement("dict");
736   this->WriteRezArray(xml, rez.LPic);
737   this->WriteRezArray(xml, rez.Menu);
738   this->WriteRezArray(xml, rez.Text);
739   this->WriteRezArray(xml, rez.RTF);
740   xml.EndElement(); // dict
741   xml.EndElement(); // plist
742   xml.EndDocument();
743   fxml.Close();
744 }
745 
WriteRezArray(cmXMLWriter & xml,RezArray const & array)746 void cmCPackDragNDropGenerator::WriteRezArray(cmXMLWriter& xml,
747                                               RezArray const& array)
748 {
749   if (array.Entries.empty()) {
750     return;
751   }
752   xml.StartElement("key");
753   xml.Content(array.Key);
754   xml.EndElement(); // key
755   xml.StartElement("array");
756   for (RezDict const& dict : array.Entries) {
757     this->WriteRezDict(xml, dict);
758   }
759   xml.EndElement(); // array
760 }
761 
WriteRezDict(cmXMLWriter & xml,RezDict const & dict)762 void cmCPackDragNDropGenerator::WriteRezDict(cmXMLWriter& xml,
763                                              RezDict const& dict)
764 {
765   std::vector<char> base64buf(dict.Data.size() * 3 / 2 + 5);
766   size_t base64len =
767     cmsysBase64_Encode(dict.Data.data(), dict.Data.size(),
768                        reinterpret_cast<unsigned char*>(base64buf.data()), 0);
769   std::string base64data(base64buf.data(), base64len);
770   /* clang-format off */
771   xml.StartElement("dict");
772   xml.StartElement("key");    xml.Content("Attributes"); xml.EndElement();
773   xml.StartElement("string"); xml.Content("0x0000");     xml.EndElement();
774   xml.StartElement("key");    xml.Content("Data");       xml.EndElement();
775   xml.StartElement("data");   xml.Content(base64data);   xml.EndElement();
776   xml.StartElement("key");    xml.Content("ID");         xml.EndElement();
777   xml.StartElement("string"); xml.Content(dict.ID);      xml.EndElement();
778   xml.StartElement("key");    xml.Content("Name");       xml.EndElement();
779   xml.StartElement("string"); xml.Content(dict.Name);    xml.EndElement();
780   xml.EndElement(); // dict
781   /* clang-format on */
782 }
783 
WriteLicense(RezDoc & rez,size_t licenseNumber,std::string licenseLanguage,const std::string & licenseFile,std::string * error)784 bool cmCPackDragNDropGenerator::WriteLicense(RezDoc& rez, size_t licenseNumber,
785                                              std::string licenseLanguage,
786                                              const std::string& licenseFile,
787                                              std::string* error)
788 {
789   if (!licenseFile.empty() && !singleLicense) {
790     licenseNumber = 5002;
791     licenseLanguage = "English";
792   }
793 
794   // License file
795   RezArray* licenseArray = &rez.Text;
796   std::string actual_license;
797   if (!licenseFile.empty()) {
798     if (cmHasLiteralSuffix(licenseFile, ".rtf")) {
799       licenseArray = &rez.RTF;
800     }
801     actual_license = licenseFile;
802   } else {
803     std::string license_wo_ext =
804       slaDirectory + "/" + licenseLanguage + ".license";
805     if (cmSystemTools::FileExists(license_wo_ext + ".txt")) {
806       actual_license = license_wo_ext + ".txt";
807     } else {
808       licenseArray = &rez.RTF;
809       actual_license = license_wo_ext + ".rtf";
810     }
811   }
812 
813   // License body
814   {
815     RezDict license = { licenseLanguage, licenseNumber, {} };
816     std::vector<std::string> lines;
817     if (!this->ReadFile(actual_license, lines, error)) {
818       return false;
819     }
820     this->EncodeLicense(license, lines);
821     licenseArray->Entries.emplace_back(std::move(license));
822   }
823 
824   // Menu body
825   {
826     RezDict menu = { licenseLanguage, licenseNumber, {} };
827     if (!licenseFile.empty() && !singleLicense) {
828       this->EncodeMenu(menu, DefaultMenu);
829     } else {
830       std::vector<std::string> lines;
831       std::string actual_menu =
832         slaDirectory + "/" + licenseLanguage + ".menu.txt";
833       if (!this->ReadFile(actual_menu, lines, error)) {
834         return false;
835       }
836       this->EncodeMenu(menu, lines);
837     }
838     rez.Menu.Entries.emplace_back(std::move(menu));
839   }
840 
841   return true;
842 }
843 
EncodeLicense(RezDict & dict,std::vector<std::string> const & lines)844 void cmCPackDragNDropGenerator::EncodeLicense(
845   RezDict& dict, std::vector<std::string> const& lines)
846 {
847   // License text uses CR newlines.
848   for (std::string const& l : lines) {
849     dict.Data.insert(dict.Data.end(), l.begin(), l.end());
850     dict.Data.push_back('\r');
851   }
852   dict.Data.push_back('\r');
853 }
854 
EncodeMenu(RezDict & dict,std::vector<std::string> const & lines)855 void cmCPackDragNDropGenerator::EncodeMenu(
856   RezDict& dict, std::vector<std::string> const& lines)
857 {
858   // Menu resources start with a big-endian uint16_t for number of lines:
859   {
860     uint16_t numLines = static_cast<uint16_t>(lines.size());
861     char* d = reinterpret_cast<char*>(&numLines);
862 #if KWIML_ABI_ENDIAN_ID == KWIML_ABI_ENDIAN_ID_LITTLE
863     dict.Data.push_back(d[1]);
864     dict.Data.push_back(d[0]);
865 #else
866     dict.Data.push_back(d[0]);
867     dict.Data.push_back(d[1]);
868 #endif
869   }
870   // Each line starts with a uint8_t length, plus the bytes themselves:
871   for (std::string const& l : lines) {
872     dict.Data.push_back(static_cast<unsigned char>(l.length()));
873     dict.Data.insert(dict.Data.end(), l.begin(), l.end());
874   }
875 }
876 
ReadFile(std::string const & file,std::vector<std::string> & lines,std::string * error)877 bool cmCPackDragNDropGenerator::ReadFile(std::string const& file,
878                                          std::vector<std::string>& lines,
879                                          std::string* error)
880 {
881   cmsys::ifstream ifs(file);
882   std::string line;
883   while (std::getline(ifs, line)) {
884     if (!this->BreakLongLine(line, lines, error)) {
885       return false;
886     }
887   }
888   return true;
889 }
890 
BreakLongLine(const std::string & line,std::vector<std::string> & lines,std::string * error)891 bool cmCPackDragNDropGenerator::BreakLongLine(const std::string& line,
892                                               std::vector<std::string>& lines,
893                                               std::string* error)
894 {
895   const size_t max_line_length = 255;
896   size_t line_length = max_line_length;
897   for (size_t i = 0; i < line.size(); i += line_length) {
898     line_length = max_line_length;
899     if (i + line_length > line.size()) {
900       line_length = line.size() - i;
901     } else {
902       while (line_length > 0 && line[i + line_length - 1] != ' ') {
903         line_length = line_length - 1;
904       }
905     }
906 
907     if (line_length == 0) {
908       *error = "Please make sure there are no words "
909                "(or character sequences not broken up by spaces or newlines) "
910                "in your license file which are more than 255 characters long.";
911       return false;
912     }
913     lines.push_back(line.substr(i, line_length));
914   }
915   return true;
916 }
917