Adjust how macOS drives are analysed and add a couple of tests.
[dcpomatic.git] / src / lib / cross_common.cc
1 /*
2     Copyright (C) 2012-2021 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21
22 #include "cross.h"
23 #include "compose.hpp"
24 #include "dcpomatic_log.h"
25 #include "warnings.h"
26 #include <dcp/raw_convert.h>
27 DCPOMATIC_DISABLE_WARNINGS
28 #include <libxml++/libxml++.h>
29 DCPOMATIC_ENABLE_WARNINGS
30 #include <boost/algorithm/string.hpp>
31 #include <iostream>
32
33 #include "i18n.h"
34
35
36 using std::map;
37 using std::string;
38 using std::vector;
39 using boost::optional;
40
41
42 Drive::Drive (string xml)
43 {
44         cxml::Document doc;
45         doc.read_string (xml);
46         _device = doc.string_child("Device");
47         for (auto i: doc.node_children("MountPoint")) {
48                 _mount_points.push_back (i->content());
49         }
50         _size = doc.number_child<uint64_t>("Size");
51         _vendor = doc.optional_string_child("Vendor");
52         _model = doc.optional_string_child("Model");
53 }
54
55
56 string
57 Drive::as_xml () const
58 {
59         xmlpp::Document doc;
60         auto root = doc.create_root_node ("Drive");
61         root->add_child("Device")->add_child_text(_device);
62         for (auto i: _mount_points) {
63                 root->add_child("MountPoint")->add_child_text(i.string());
64         }
65         root->add_child("Size")->add_child_text(dcp::raw_convert<string>(_size));
66         if (_vendor) {
67                 root->add_child("Vendor")->add_child_text(*_vendor);
68         }
69         if (_model) {
70                 root->add_child("Model")->add_child_text(*_model);
71         }
72
73         return doc.write_to_string("UTF-8");
74 }
75
76
77 string
78 Drive::description () const
79 {
80         char gb[64];
81         snprintf(gb, 64, "%.1f", _size / 1000000000.0);
82
83         string name;
84         if (_vendor) {
85                 name += *_vendor;
86         }
87         if (_model) {
88                 if (name.size() > 0) {
89                         name += " " + *_model;
90                 } else {
91                         name = *_model;
92                 }
93         }
94         if (name.size() == 0) {
95                 name = _("Unknown");
96         }
97
98         return String::compose(_("%1 (%2 GB) [%3]"), name, gb, _device);
99 }
100
101
102 string
103 Drive::log_summary () const
104 {
105         string mp;
106         for (auto i: _mount_points) {
107                 mp += i.string() + ",";
108         }
109         if (mp.empty()) {
110                 mp = "[none]";
111         } else {
112                 mp = mp.substr (0, mp.length() - 1);
113         }
114
115         return String::compose(
116                 "Device %1 mounted on %2 size %3 vendor %4 model %5",
117                 _device, mp, _size, _vendor.get_value_or("[none]"), _model.get_value_or("[none]")
118                         );
119 }
120
121
122
123 /* This is in _common so we can use it in unit tests */
124 optional<OSXMediaPath>
125 analyse_osx_media_path (string path)
126 {
127         if (path.find("/IOHDIXController") != string::npos) {
128                 /* This is a disk image, so we completely ignore it */
129                 LOG_DISK_NC("Ignoring this as it seems to be a disk image");
130                 return {};
131         }
132
133         OSXMediaPath mp;
134         vector<string> parts;
135         split(parts, path, boost::is_any_of("/"));
136         std::copy(parts.begin() + 1, parts.end(), back_inserter(mp.parts));
137
138         if (!parts.empty() && parts[0] == "IODeviceTree:") {
139                 mp.real = true;
140                 if (mp.parts.size() < 2) {
141                         /* Later we expect at least 2 parts in a IODeviceTree */
142                         LOG_DISK_NC("Ignoring this as it has a strange media path");
143                         return {};
144                 }
145         } else if (!parts.empty() && parts[0] == "IOService:") {
146                 mp.real = false;
147         } else {
148                 return {};
149         }
150
151         return mp;
152 }
153
154
155 /* Take soem OSXDisk objects, representing disks that `DARegisterDiskAppearedCallback` told us about,
156  * and find those drives that we could write a DCP to.  The drives returned are "real" (not synthesized)
157  * and are whole disks (not partitions).  They may be mounted, or contain mounted partitions, in which
158  * their mounted() method will return true.
159  */
160 vector<Drive>
161 osx_disks_to_drives (vector<OSXDisk> disks)
162 {
163         using namespace boost::algorithm;
164
165         /* Mark disks containing mounted partitions as themselves mounted */
166         for (auto& i: disks) {
167                 if (!i.whole) {
168                         continue;
169                 }
170                 for (auto& j: disks) {
171                         if (!j.mount_points.empty() && starts_with(j.device, i.device)) {
172                                 LOG_DISK("Marking %1 as mounted because %2 is", i.device, j.device);
173                                 std::copy(j.mount_points.begin(), j.mount_points.end(), back_inserter(i.mount_points));
174                         }
175                 }
176         }
177
178         /* Mark containers of mounted synths as themselves mounted */
179         for (auto& i: disks) {
180                 if (i.media_path.real) {
181                         for (auto& j: disks) {
182                                 if (!j.media_path.real && !j.mount_points.empty()) {
183                                         /* i is real, j is a mounted synth; if we see the first two parts
184                                          * of i anywhere in j we assume they are related and so i shares
185                                          * j's mount points.
186                                          */
187                                         if (
188                                                 find(j.media_path.parts.begin(), j.media_path.parts.end(), i.media_path.parts[0]) != j.media_path.parts.end() &&
189                                                 find(j.media_path.parts.begin(), j.media_path.parts.end(), i.media_path.parts[1]) != j.media_path.parts.end()) {
190                                                 LOG_DISK("Marking %1 as mounted because %2 is (found %3 and %4)", i.device, j.device, i.media_path.parts[0], i.media_path.parts[1]);
191                                                 std::copy(j.mount_points.begin(), j.mount_points.end(), back_inserter(i.mount_points));
192                                         }
193                                 }
194                         }
195                 }
196         }
197
198         vector<Drive> drives;
199         for (auto const& i: disks) {
200                 if (i.whole && i.media_path.real) {
201                         drives.push_back(Drive(i.device, i.mount_points, i.size, i.vendor, i.model));
202                         LOG_DISK_NC(drives.back().log_summary());
203                 }
204         }
205
206         return drives;
207 }