Branch data Line data Source code
1 : : /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
2 : : *
3 : : * Copyright 2024, 2025 GNOME Foundation, Inc.
4 : : *
5 : : * SPDX-License-Identifier: LGPL-2.1-or-later
6 : : *
7 : : * This library is free software; you can redistribute it and/or
8 : : * modify it under the terms of the GNU Lesser General Public
9 : : * License as published by the Free Software Foundation; either
10 : : * version 2.1 of the License, or (at your option) any later version.
11 : : *
12 : : * This library is distributed in the hope that it will be useful,
13 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 : : * Lesser General Public License for more details.
16 : : *
17 : : * You should have received a copy of the GNU Lesser General Public
18 : : * License along with this library; if not, write to the Free Software
19 : : * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20 : : *
21 : : * Authors:
22 : : * - Philip Withnall <pwithnall@gnome.org>
23 : : */
24 : :
25 : : #include "config.h"
26 : :
27 : : #include <glib.h>
28 : : #include <glib/gi18n-lib.h>
29 : : #include <glib-object.h>
30 : : #include <gio/gio.h>
31 : : #include <gvdb/gvdb-builder.h>
32 : : #include <gvdb/gvdb-reader.h>
33 : : #include <libmalcontent-timer/timer-store.h>
34 : :
35 : :
36 : : static const struct
37 : : {
38 : : const char *str;
39 : : }
40 : : record_types[] =
41 : : {
42 : : [MCT_TIMER_STORE_RECORD_TYPE_LOGIN_SESSION] = {
43 : : .str = "login-session",
44 : : },
45 : : [MCT_TIMER_STORE_RECORD_TYPE_APP] = {
46 : : .str = "app",
47 : : },
48 : : };
49 : :
50 : : /**
51 : : * mct_timer_store_record_type_to_string:
52 : : * @record_type: a record type
53 : : *
54 : : * Gets the string form of @record_type.
55 : : *
56 : : * Returns: string version of @record_type
57 : : * Since: 0.14.0
58 : : */
59 : : const char *
60 : 0 : mct_timer_store_record_type_to_string (MctTimerStoreRecordType record_type)
61 : : {
62 : 0 : return record_types[record_type].str;
63 : : }
64 : :
65 : : /**
66 : : * mct_timer_store_record_type_from_string:
67 : : * @str: a string representing a record type
68 : : *
69 : : * Converts @str to a [type@Malcontent.TimerStoreRecordType].
70 : : *
71 : : * It is an error to call this with a string which does not represent a
72 : : * [type@Malcontent.TimerStoreRecordType].
73 : : *
74 : : * Returns: a record type
75 : : * Since: 0.14.0
76 : : */
77 : : MctTimerStoreRecordType
78 : 0 : mct_timer_store_record_type_from_string (const char *str)
79 : : {
80 [ # # ]: 0 : for (size_t i = 0; i < G_N_ELEMENTS (record_types); i++)
81 : : {
82 [ # # ]: 0 : if (g_str_equal (str, record_types[i].str))
83 : 0 : return (MctTimerStoreRecordType) i;
84 : : }
85 : :
86 : : g_assert_not_reached ();
87 : : }
88 : :
89 : : /**
90 : : * mct_timer_store_record_type_validate_string:
91 : : * @record_type_str: a string potentially representing a record type
92 : : * @error: return location for an error, or `NULL` to ignore
93 : : *
94 : : * Validates whether @record_type_str is a valid
95 : : * [type@Malcontent.TimerStoreRecordType].
96 : : *
97 : : * If @record_type_str is not a valid record type string,
98 : : * [error@Gio.IOErrorEnum.INVALID_DATA] is returned.
99 : : *
100 : : * Returns: true if @record_type_str is valid, false otherwise
101 : : * Since: 0.14.0
102 : : */
103 : : gboolean
104 : 0 : mct_timer_store_record_type_validate_string (const char *record_type_str,
105 : : GError **error)
106 : : {
107 [ # # ]: 0 : for (size_t i = 0; i < G_N_ELEMENTS (record_types); i++)
108 : : {
109 [ # # ]: 0 : if (g_strcmp0 (record_types[i].str, record_type_str) == 0)
110 : 0 : return TRUE;
111 : : }
112 : :
113 : : /* Couldn’t find it */
114 : 0 : g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
115 : : _("Invalid record type ‘%s’"), record_type_str);
116 : 0 : return FALSE;
117 : : }
118 : :
119 : : /**
120 : : * mct_timer_store_record_type_validate_identifier:
121 : : * @record_type: a record type
122 : : * @identifier: identifier potentially in the format required by @record_type
123 : : * @error: return location for an error, or `NULL` to ignore
124 : : *
125 : : * Validates whether @identifier is in a valid format for @record_type.
126 : : *
127 : : * Different record types have different identifier formats.
128 : : *
129 : : * If @identifier is not in a valid format for @record_type,
130 : : * [error@Gio.IOErrorEnum.INVALID_DATA] is returned.
131 : : *
132 : : * Returns: true if @identifier is valid for @record_type, false otherwise
133 : : * Since: 0.14.0
134 : : */
135 : : gboolean
136 : 0 : mct_timer_store_record_type_validate_identifier (MctTimerStoreRecordType record_type,
137 : : const char *identifier,
138 : : GError **error)
139 : : {
140 [ # # # # : 0 : if ((record_type == MCT_TIMER_STORE_RECORD_TYPE_LOGIN_SESSION && *identifier != '\0') ||
# # ]
141 [ # # ]: 0 : (record_type == MCT_TIMER_STORE_RECORD_TYPE_APP && !g_application_id_is_valid (identifier)))
142 : : {
143 : 0 : g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
144 : : _("Invalid identifier ‘%s’"), identifier);
145 : 0 : return FALSE;
146 : : }
147 : :
148 : 0 : return TRUE;
149 : : }
150 : :
151 : :
152 : : static void mct_timer_store_constructed (GObject *object);
153 : : static void mct_timer_store_dispose (GObject *object);
154 : : static void mct_timer_store_finalize (GObject *object);
155 : :
156 : : static void mct_timer_store_get_property (GObject *object,
157 : : guint property_id,
158 : : GValue *value,
159 : : GParamSpec *pspec);
160 : : static void mct_timer_store_set_property (GObject *object,
161 : : guint property_id,
162 : : const GValue *value,
163 : : GParamSpec *pspec);
164 : :
165 : : /**
166 : : * MctTimerStore:
167 : : *
168 : : * A data store which contains screen time usage data for multiple users.
169 : : *
170 : : * It guarantees to be atomic for reads or writes on a single user’s data. Reads
171 : : * or writes across multiple users are not atomic.
172 : : *
173 : : * A user’s store file has to be explicitly opened before it can be modified.
174 : : * All modifications made to the file while it’s opened are queued up and
175 : : * applied atomically when it’s closed. This allows the edits to be atomic, and
176 : : * means errors are all handled in one place.
177 : : *
178 : : * It’s supported to have more than one user’s store file open simultaneously,
179 : : * but simultaneous operations from two clients on the same user’s file are not
180 : : * supported and will result in a [error@Gio.IOErrorEnum.BUSY] error.
181 : : *
182 : : * ## Format
183 : : *
184 : : * The database format is not part of the public API, but is documented here for
185 : : * simplicity.
186 : : *
187 : : * There is one GVDB file per username, all stored in the same directory. Each
188 : : * file is memory mapped for reads, and atomically overwritten when saved to.
189 : : *
190 : : * Inside each GVDB file is a table for each record type. Each entry in the
191 : : * table is a mapping from an identifier (which may be the empty string) to a
192 : : * variant of type `a(tt)`, which is the array of time spans. Each element in
193 : : * the variant is conceptually a [struct@Malcontent.TimeSpan].
194 : : *
195 : : * When a GVDB file is written, the time spans are coalesced (so overlapping
196 : : * time spans are combined), then trimmed against an expiry cutoff (so old data
197 : : * expires) and sorted in increasing order of start (then end) time.
198 : : *
199 : : * There is no metadata header in these GVDB files. If the database format needs
200 : : * to change in future, a new version can be put into a versioned subdirectory.
201 : : *
202 : : * Since: 0.14.0
203 : : */
204 : : struct _MctTimerStore
205 : : {
206 : : GObject parent;
207 : :
208 : : GFile *store_directory; /* (owned) (not nullable) */
209 : :
210 : : /* Map from username to (
211 : : * map from MctTimerStoreRecordType to (
212 : : * map from identifier (utf8) to (
213 : : * array of MctTimeSpans
214 : : * )
215 : : * )
216 : : * )
217 : : */
218 : : GHashTable *open_data; /* (owned) (nullable) (element-type utf8 GHashTable<MctTimerStoreRecordType, GHashTable<utf8,GPtrArray<MctTimeSpan>>>) */
219 : : };
220 : :
221 : : typedef enum
222 : : {
223 : : PROP_STORE_DIRECTORY = 1,
224 : : } MctTimerStoreProperty;
225 : :
226 : : static GParamSpec *props[PROP_STORE_DIRECTORY + 1] = { NULL, };
227 : :
228 : : typedef enum
229 : : {
230 : : SIGNAL_ESTIMATED_END_TIMES_CHANGED = 0,
231 : : } MctTimerStoreSignal;
232 : :
233 : : static unsigned int signals[SIGNAL_ESTIMATED_END_TIMES_CHANGED + 1] = { 0, };
234 : :
235 [ + + + - : 22 : G_DEFINE_TYPE (MctTimerStore, mct_timer_store, G_TYPE_OBJECT)
+ + ]
236 : :
237 : : static void
238 : 2 : mct_timer_store_class_init (MctTimerStoreClass *klass)
239 : : {
240 : 2 : GObjectClass *object_class = (GObjectClass *) klass;
241 : :
242 : 2 : object_class->constructed = mct_timer_store_constructed;
243 : 2 : object_class->dispose = mct_timer_store_dispose;
244 : 2 : object_class->finalize = mct_timer_store_finalize;
245 : 2 : object_class->get_property = mct_timer_store_get_property;
246 : 2 : object_class->set_property = mct_timer_store_set_property;
247 : :
248 : : /**
249 : : * MctTimerStore:store-directory: (not nullable)
250 : : *
251 : : * The directory which contains the timer store.
252 : : *
253 : : * Since: 0.14.0
254 : : */
255 : 2 : props[PROP_STORE_DIRECTORY] =
256 : 2 : g_param_spec_object ("store-directory", NULL, NULL,
257 : : G_TYPE_FILE,
258 : : G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
259 : :
260 : 2 : g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props);
261 : :
262 : : /**
263 : : * MctTimerStore::estimated-end-times-changed:
264 : : * @self: a #MctTimerStore
265 : : * @username: name of the user for whom the estimated end times changed
266 : : *
267 : : * Emitted when any of the estimated end times for the given user might have
268 : : * changed.
269 : : *
270 : : * Typically you will want to call
271 : : * [method@Mct.TimerStore.calculate_total_times_between] on receiving this
272 : : * signal.
273 : : *
274 : : * Since: 0.14.0
275 : : */
276 : 2 : signals[SIGNAL_ESTIMATED_END_TIMES_CHANGED] =
277 : 2 : g_signal_new ("estimated-end-times-changed", G_TYPE_FROM_CLASS (klass),
278 : : G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
279 : : G_TYPE_NONE, 1, G_TYPE_STRING);
280 : 2 : }
281 : :
282 : : static void
283 : 2 : mct_timer_store_init (MctTimerStore *self)
284 : : {
285 : 2 : self->open_data = g_hash_table_new_full (g_str_hash, g_str_equal,
286 : : g_free, (GDestroyNotify) g_hash_table_unref);
287 : 2 : }
288 : :
289 : : static void
290 : 2 : mct_timer_store_constructed (GObject *object)
291 : : {
292 : 2 : MctTimerStore *self = MCT_TIMER_STORE (object);
293 : :
294 : 2 : G_OBJECT_CLASS (mct_timer_store_parent_class)->constructed (object);
295 : :
296 : : /* Check we have our construction properties. */
297 : 2 : g_assert (G_IS_FILE (self->store_directory));
298 : 2 : }
299 : :
300 : : static void
301 : 1 : mct_timer_store_dispose (GObject *object)
302 : : {
303 : 1 : MctTimerStore *self = MCT_TIMER_STORE (object);
304 : :
305 [ + - ]: 1 : g_clear_object (&self->store_directory);
306 : :
307 : : /* Chain up to the parent class */
308 : 1 : G_OBJECT_CLASS (mct_timer_store_parent_class)->dispose (object);
309 : 1 : }
310 : :
311 : : static void
312 : 1 : mct_timer_store_finalize (GObject *object)
313 : : {
314 : 1 : MctTimerStore *self = MCT_TIMER_STORE (object);
315 : :
316 : : /* Should have been saved already. */
317 : 1 : g_assert (g_hash_table_size (self->open_data) == 0);
318 [ + - ]: 1 : g_clear_pointer (&self->open_data, g_hash_table_unref);
319 : :
320 : : /* Chain up to the parent class */
321 : 1 : G_OBJECT_CLASS (mct_timer_store_parent_class)->finalize (object);
322 : 1 : }
323 : :
324 : : static void
325 : 0 : mct_timer_store_get_property (GObject *object,
326 : : guint property_id,
327 : : GValue *value,
328 : : GParamSpec *pspec)
329 : : {
330 : 0 : MctTimerStore *self = MCT_TIMER_STORE (object);
331 : :
332 [ # # ]: 0 : switch ((MctTimerStoreProperty) property_id)
333 : : {
334 : 0 : case PROP_STORE_DIRECTORY:
335 : 0 : g_value_set_object (value, self->store_directory);
336 : 0 : break;
337 : 0 : default:
338 : : g_assert_not_reached ();
339 : : }
340 : 0 : }
341 : :
342 : : static void
343 : 2 : mct_timer_store_set_property (GObject *object,
344 : : guint property_id,
345 : : const GValue *value,
346 : : GParamSpec *pspec)
347 : : {
348 : 2 : MctTimerStore *self = MCT_TIMER_STORE (object);
349 : :
350 [ + - ]: 2 : switch ((MctTimerStoreProperty) property_id)
351 : : {
352 : 2 : case PROP_STORE_DIRECTORY:
353 : : /* Construct only. */
354 : 2 : g_assert (self->store_directory == NULL);
355 : 2 : self->store_directory = g_value_dup_object (value);
356 : 2 : break;
357 : 0 : default:
358 : : g_assert_not_reached ();
359 : : }
360 : 2 : }
361 : :
362 : : /**
363 : : * mct_timer_store_new:
364 : : * @store_directory: (transfer none) (not nullable): directory containing the timer store
365 : : *
366 : : * Create a new [class@Malcontent.TimerStore] instance which stores its data in
367 : : * @store_directory.
368 : : *
369 : : * Returns: (transfer full): a new [class@Malcontent.TimerStore]
370 : : * Since: 0.14.0
371 : : */
372 : : MctTimerStore *
373 : 2 : mct_timer_store_new (GFile *store_directory)
374 : : {
375 : 2 : g_return_val_if_fail (G_IS_FILE (store_directory), NULL);
376 : :
377 : 2 : return g_object_new (MCT_TYPE_TIMER_STORE,
378 : : "store-directory", store_directory,
379 : : NULL);
380 : : }
381 : :
382 : : /**
383 : : * mct_timer_store_get_store_directory:
384 : : * @self: a [class@Malcontent.TimerStore]
385 : : *
386 : : * Get the value of [prop@Malcontent.TimerStore.store-directory].
387 : : *
388 : : * Returns: (transfer none) (not nullable): the directory storing the data for
389 : : * the timer store
390 : : * Since: 0.14.0
391 : : */
392 : : GFile *
393 : 0 : mct_timer_store_get_store_directory (MctTimerStore *self)
394 : : {
395 : 0 : g_return_val_if_fail (MCT_IS_TIMER_STORE (self), NULL);
396 : :
397 : 0 : return self->store_directory;
398 : : }
399 : :
400 : : static gboolean
401 : 2 : validate_username (const char *username)
402 : : {
403 : : /* Ideally we should validate against the relaxed mode rules followed by
404 : : * systemd (https://systemd.io/USER_NAMES/#relaxed-mode), but for the moment
405 : : * all that we’re really concerned about is that the username can’t be used
406 : : * for directory traversal. */
407 [ + - + - ]: 4 : return (username != NULL && *username != '\0' &&
408 : 2 : g_utf8_validate (username, -1, NULL) &&
409 [ + - + - ]: 6 : strchr (username, '.') == NULL &&
410 [ + - ]: 2 : strchr (username, '/') == NULL);
411 : : }
412 : :
413 : : static GFile *
414 : 2 : get_database_file_for_username (MctTimerStore *self,
415 : : const char *username)
416 : : {
417 : 4 : g_autofree char *filename = g_strconcat (username, ".gvdb", NULL);
418 : 2 : return g_file_get_child (self->store_directory, filename);
419 : : }
420 : :
421 : : /**
422 : : * mct_timer_store_open_username_async:
423 : : * @self: a [class@Malcontent.TimerStore]
424 : : * @username: username to open the database file of
425 : : * @cancellable: a cancellable
426 : : * @callback: callback function to invoke once the asynchronous operation is
427 : : * complete
428 : : * @user_data: data to pass to @callback
429 : : *
430 : : * Open the database file for @username for reading and writing.
431 : : *
432 : : * This is an asynchronous operation and must be completed by calling
433 : : * [method@Malcontent.TimerStore.open_username_finish]. On success, it will
434 : : * return a transaction handle which must be passed to exactly one of
435 : : * [method@Malcontent.TimerStore.save_transaction_async] or
436 : : * [method@Malcontent.TimerStore.roll_back_transaction] to commit or abort the
437 : : * transaction once you’ve finished adding time spans or calculating for the
438 : : * user.
439 : : *
440 : : * If this method returns an error, you do not need to call
441 : : * [method@Malcontent.TimerStore.save_transaction_async] or
442 : : * [method@Malcontent.TimerStore.roll_back_transaction].
443 : : *
444 : : * If no database file exists for @username, one is created.
445 : : *
446 : : * The asynchronous operation can return [error@GLib.FileError] if there was a
447 : : * file system error opening an existing database file. In particular, if the
448 : : * database file exists but is corrupt, [error@GLib.FileError.INVAL] will be
449 : : * returned. If another caller already has the file open for @username,
450 : : * [error@Gio.IOErrorEnum.BUSY] will be returned.
451 : : *
452 : : * Since: 0.14.0
453 : : */
454 : : void
455 : 2 : mct_timer_store_open_username_async (MctTimerStore *self,
456 : : const char *username,
457 : : GCancellable *cancellable,
458 : : GAsyncReadyCallback callback,
459 : : void *user_data)
460 : : {
461 [ + - ]: 2 : g_autoptr(GTask) task = NULL;
462 [ + - ]: 2 : g_autoptr(GFile) database_file = NULL;
463 [ + - ]: 2 : g_autoptr(GvdbTable) database_table = NULL;
464 [ + - ]: 2 : g_autoptr(GError) local_error = NULL;
465 [ + - ]: 2 : g_autoptr(GHashTable) data = NULL; /* (element-type MctTimerStoreRecordType GHashTable<utf8, GPtrArray<MctTimeSpan>>) */
466 [ + - ]: 2 : g_autofree char *transaction_key_owned = NULL;
467 : : const char *transaction_key;
468 : :
469 : 2 : g_return_if_fail (MCT_IS_TIMER_STORE (self));
470 : 2 : g_return_if_fail (validate_username (username));
471 : 2 : g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
472 : :
473 : 2 : task = g_task_new (self, cancellable, callback, user_data);
474 [ + - ]: 2 : g_task_set_source_tag (task, mct_timer_store_open_username_async);
475 : :
476 : : /* Shouldn’t already have this user open. */
477 [ - + ]: 2 : if (g_hash_table_contains (self->open_data, username))
478 : : {
479 : 0 : g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_BUSY,
480 : 0 : _("Database is already open for ‘%s’"), username);
481 : 0 : return;
482 : : }
483 : :
484 : : /* Load the existing file for this user. This is sync (memory mapped) for now,
485 : : * but the timer store API allows for us to make it explicitly async in future
486 : : * if needed. */
487 : 2 : database_file = get_database_file_for_username (self, username);
488 : 2 : database_table = gvdb_table_new (g_file_peek_path (database_file), TRUE, &local_error);
489 : :
490 [ + - - + ]: 4 : if (database_table == NULL &&
491 : 2 : !g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
492 : : {
493 : 0 : g_task_return_error (task, g_steal_pointer (&local_error));
494 : 0 : return;
495 : : }
496 : :
497 : 2 : data = g_hash_table_new_full (g_direct_hash, g_direct_equal,
498 : : NULL, (GDestroyNotify) g_hash_table_unref);
499 : :
500 [ + + ]: 6 : for (size_t i = 0; i < G_N_ELEMENTS (record_types); i++)
501 : : {
502 : 4 : const char *record_type = record_types[i].str;
503 [ + - ]: 4 : g_autoptr(GvdbTable) record_table = NULL;
504 [ + - ]: 4 : g_auto(GStrv) identifiers = NULL;
505 : : GHashTable *record_data; /* (element-type utf8 GPtrArray<MctTimeSpan>) */
506 [ + - ]: 4 : g_autoptr(GHashTable) record_data_owned = NULL; /* (element-type utf8 GPtrArray<MctTimeSpan>) */
507 : :
508 [ - + ]: 4 : record_table = (database_table != NULL) ? gvdb_table_get_table (database_table, record_type) : NULL;
509 [ - + ]: 4 : identifiers = (record_table != NULL) ? gvdb_table_get_names (record_table, NULL) : NULL;
510 : :
511 : 4 : record_data = record_data_owned = g_hash_table_new_full (g_str_hash, g_str_equal,
512 : : g_free, (GDestroyNotify) g_ptr_array_unref);
513 : 4 : g_hash_table_insert (data, GINT_TO_POINTER (i), g_steal_pointer (&record_data_owned));
514 : :
515 [ - + - - ]: 4 : for (size_t j = 0; identifiers != NULL && identifiers[j] != NULL; j++)
516 : : {
517 : 0 : const char *identifier = identifiers[j];
518 [ # # ]: 0 : g_autoptr(GVariant) time_spans_variant = NULL;
519 : : GVariantIter time_spans_iter;
520 [ # # ]: 0 : g_autoptr(GPtrArray) time_spans_array = NULL; /* (element-type MctTimeSpan) */
521 : : uint64_t start_time, end_time;
522 : :
523 : : /* Load the time spans into an array for now, for ease of modification.
524 : : * In future, this could be expanded to keep them as a GVariant until
525 : : * modified (copy-on-write), since the typical use case will only be
526 : : * to add spans to one or two identifiers. */
527 : 0 : time_spans_variant = gvdb_table_get_value (record_table, identifier);
528 : :
529 [ # # ]: 0 : if (!g_variant_is_of_type (time_spans_variant, G_VARIANT_TYPE ("a(tt)")))
530 : : {
531 : 0 : g_task_return_new_error (task, G_FILE_ERROR, G_FILE_ERROR_INVAL,
532 : 0 : _("Corrupt file ‘%s’"), g_file_peek_path (database_file));
533 : 0 : return;
534 : : }
535 : :
536 : 0 : g_variant_iter_init (&time_spans_iter, time_spans_variant);
537 : 0 : time_spans_array = g_ptr_array_new_full (g_variant_n_children (time_spans_variant), (GDestroyNotify) mct_time_span_free);
538 : :
539 [ # # ]: 0 : while (g_variant_iter_loop (&time_spans_iter, "(tt)", &start_time, &end_time))
540 : 0 : g_ptr_array_add (time_spans_array, mct_time_span_new (start_time, end_time));
541 : :
542 : 0 : g_hash_table_insert (record_data, g_strdup (identifier), g_steal_pointer (&time_spans_array));
543 : : }
544 : : }
545 : :
546 : : /* Mark the user’s file as open */
547 : 2 : transaction_key = transaction_key_owned = g_strdup (username);
548 : 2 : g_hash_table_insert (self->open_data,
549 : : g_steal_pointer (&transaction_key_owned), g_steal_pointer (&data));
550 : :
551 : 2 : g_task_return_pointer (task, (void *) transaction_key, NULL);
552 : : }
553 : :
554 : : /**
555 : : * mct_timer_store_open_username_finish:
556 : : * @self: a [class@Malcontent.TimerStore]
557 : : * @result: result of the asynchronous operation
558 : : * @error: return location for an error, or `NULL` to ignore
559 : : *
560 : : * Finish an asynchronous operation started with
561 : : * [method@Malcontent.TimerStore.open_username_async].
562 : : *
563 : : * See the documentation for that method for details of the return value and
564 : : * possible errors.
565 : : *
566 : : * Returns: an opaque transaction identifier, or `NULL` on failure
567 : : * Since: 0.14.0
568 : : */
569 : : const MctTimerStoreTransaction *
570 : 2 : mct_timer_store_open_username_finish (MctTimerStore *self,
571 : : GAsyncResult *result,
572 : : GError **error)
573 : : {
574 : 2 : g_return_val_if_fail (MCT_IS_TIMER_STORE (self), FALSE);
575 : 2 : g_return_val_if_fail (g_task_is_valid (result, self), FALSE);
576 : 2 : g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
577 : :
578 : 2 : return g_task_propagate_pointer (G_TASK (result), error);
579 : : }
580 : :
581 : : static void
582 : 0 : coalesce_and_sort_time_spans (GPtrArray *time_spans_array)
583 : : {
584 : : /* Sort in ascending order of start time then by ascending order of end time */
585 : 0 : g_ptr_array_sort_values (time_spans_array, (GCompareFunc) mct_time_span_compare);
586 : :
587 : : /* Coalesce time spans which overlap */
588 [ # # ]: 0 : for (unsigned int i = 1; i < time_spans_array->len; /* increment is conditional, below */)
589 : : {
590 : 0 : MctTimeSpan *a = g_ptr_array_index (time_spans_array, i - 1);
591 : 0 : MctTimeSpan *b = g_ptr_array_index (time_spans_array, i);
592 : 0 : const uint64_t a_start = mct_time_span_get_start_time_secs (a);
593 : 0 : const uint64_t a_end = mct_time_span_get_end_time_secs (a);
594 : 0 : const uint64_t b_start = mct_time_span_get_start_time_secs (b);
595 : 0 : const uint64_t b_end = mct_time_span_get_end_time_secs (b);
596 : :
597 : 0 : g_debug ("Considering spans %" G_GUINT64_FORMAT "-%" G_GUINT64_FORMAT " and %" G_GUINT64_FORMAT "-%" G_GUINT64_FORMAT,
598 : : a_start, a_end, b_start, b_end);
599 : :
600 [ # # ]: 0 : if (a_end >= b_start)
601 : : {
602 : 0 : g_debug (" ⇒ merging");
603 : : /* Merge them */
604 [ # # ]: 0 : g_clear_pointer (&time_spans_array->pdata[i - 1], mct_time_span_free);
605 : 0 : time_spans_array->pdata[i - 1] = mct_time_span_new (a_start, MAX (a_end, b_end));
606 : 0 : g_ptr_array_remove_index (time_spans_array, i);
607 : : }
608 : : else
609 : : {
610 : 0 : i++;
611 : 0 : continue;
612 : : }
613 : : }
614 : 0 : }
615 : :
616 : : /* The input @time_spans_array must be sorted as by coalesce_and_sort_time_spans() */
617 : : static void
618 : 0 : trim_expired_time_spans (GPtrArray *time_spans_array,
619 : : uint64_t expiry_cutoff_secs)
620 : : {
621 : 0 : size_t new_first_element = 0;
622 : :
623 [ # # ]: 0 : for (unsigned int i = 0; i < time_spans_array->len; i++)
624 : : {
625 : 0 : MctTimeSpan *a = g_ptr_array_index (time_spans_array, i);
626 : 0 : const uint64_t a_start = mct_time_span_get_start_time_secs (a);
627 : 0 : const uint64_t a_end = mct_time_span_get_end_time_secs (a);
628 : :
629 : 0 : g_debug ("Considering span %" G_GUINT64_FORMAT "-%" G_GUINT64_FORMAT,
630 : : a_start, a_end);
631 : :
632 [ # # ]: 0 : if (a_end <= expiry_cutoff_secs)
633 : : {
634 : 0 : g_debug (" ⇒ removing as completely expired");
635 : 0 : new_first_element = i + 1;
636 : : }
637 [ # # ]: 0 : else if (a_start <= expiry_cutoff_secs)
638 : : {
639 : 0 : g_debug (" ⇒ trimming as partially expired");
640 [ # # ]: 0 : g_clear_pointer (&time_spans_array->pdata[i], mct_time_span_free);
641 : 0 : time_spans_array->pdata[i] = mct_time_span_new (expiry_cutoff_secs, a_end);
642 : : }
643 : : else
644 : : {
645 : : /* We’ve reached entries which are entirely after the cutoff */
646 : 0 : break;
647 : : }
648 : : }
649 : :
650 : : /* Shift the array to remove the first @new_first_element entries */
651 [ # # ]: 0 : if (new_first_element > 0)
652 : 0 : g_ptr_array_remove_range (time_spans_array, 0, new_first_element);
653 : 0 : }
654 : :
655 : : static void save_transaction_write_contents_cb (GObject *object,
656 : : GAsyncResult *result,
657 : : void *user_data);
658 : :
659 : : typedef struct
660 : : {
661 : : char *username; /* (owned) (not nullable) */
662 : : GHashTable *gvdb_data_table; /* (owned) (not nullable) */
663 : : } SaveTransactionData;
664 : :
665 : : static void
666 : 0 : save_transaction_data_free (SaveTransactionData *data)
667 : : {
668 [ # # ]: 0 : g_clear_pointer (&data->gvdb_data_table, g_hash_table_unref);
669 [ # # ]: 0 : g_clear_pointer (&data->username, g_free);
670 : 0 : g_free (data);
671 : 0 : }
672 : :
673 [ # # ]: 0 : G_DEFINE_AUTOPTR_CLEANUP_FUNC (SaveTransactionData, save_transaction_data_free)
674 : :
675 : : static SaveTransactionData *
676 : 0 : save_transaction_data_new (const char *username,
677 : : GHashTable *gvdb_data_table)
678 : : {
679 : 0 : g_autoptr(SaveTransactionData) data = g_new0 (SaveTransactionData, 1);
680 : 0 : data->username = g_strdup (username);
681 : 0 : data->gvdb_data_table = g_hash_table_ref (gvdb_data_table);
682 : 0 : return g_steal_pointer (&data);
683 : : }
684 : :
685 : : /**
686 : : * mct_timer_store_save_transaction_async:
687 : : * @self: a timer store
688 : : * @transaction: an open transaction handle
689 : : * @expiry_cutoff_secs: earliest time to save (entries before this are trimmed),
690 : : * or zero to not expire entries
691 : : * @cancellable: (nullable): a cancellable, or `NULL` to ignore
692 : : * @callback: callback to call when the async operation completes
693 : : * @user_data: data to pass to @callback
694 : : *
695 : : * Save the currently open transaction to disk.
696 : : *
697 : : * This saves all pending changes to the data for @transaction, which must have
698 : : * been started by calling [method@Mct.TimerStore.open_username_async].
699 : : *
700 : : * The changes are kept in memory if saving to disk fails. If so, a
701 : : * [error@Gio.IOErrorEnum] is returned and you may try saving again by calling
702 : : * this method again. The final save operation must succeed, or
703 : : * [method@Mct.TimerStore.roll_back_transaction] called, before the
704 : : * [class@Mct.TimerStore] is disposed.
705 : : *
706 : : * Since: 0.14.0
707 : : */
708 : : void
709 : 0 : mct_timer_store_save_transaction_async (MctTimerStore *self,
710 : : const MctTimerStoreTransaction *transaction,
711 : : uint64_t expiry_cutoff_secs,
712 : : GCancellable *cancellable,
713 : : GAsyncReadyCallback callback,
714 : : void *user_data)
715 : : {
716 [ # # ]: 0 : g_autoptr(GTask) task = NULL;
717 : : const char *username;
718 : : GHashTable *open_user_data; /* (element-type MctTimerStoreRecordType, GHashTable<utf8,GPtrArray<MctTimeSpan>>) */
719 : : GHashTableIter data_iter;
720 : : void *record_type_ptr;
721 : : GHashTable *record_data;
722 [ # # ]: 0 : g_autoptr(GHashTable) gvdb_data_table = NULL;
723 [ # # ]: 0 : g_autoptr(GFile) database_file = NULL;
724 [ # # ]: 0 : g_autofree char *database_directory_path = NULL;
725 [ # # ]: 0 : g_autoptr(GError) local_error = NULL;
726 : :
727 : 0 : g_return_if_fail (MCT_IS_TIMER_STORE (self));
728 : 0 : g_return_if_fail (transaction != NULL);
729 : 0 : g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
730 : :
731 : : /* Must already have an open user. */
732 : 0 : username = (const char *) transaction;
733 : 0 : open_user_data = g_hash_table_lookup (self->open_data, username);
734 : 0 : g_return_if_fail (open_user_data != NULL);
735 : :
736 : 0 : task = g_task_new (self, cancellable, callback, user_data);
737 [ # # ]: 0 : g_task_set_source_tag (task, mct_timer_store_save_transaction_async);
738 : 0 : g_task_set_task_data (task, (void *) username, NULL);
739 : :
740 : : /* Iterate over the internal data and turn it into a GVDB table. */
741 : 0 : g_hash_table_iter_init (&data_iter, open_user_data);
742 : 0 : gvdb_data_table = gvdb_hash_table_new (NULL, NULL);
743 : :
744 [ # # ]: 0 : while (g_hash_table_iter_next (&data_iter, &record_type_ptr, (void **) &record_data))
745 : : {
746 : 0 : MctTimerStoreRecordType record_type = GPOINTER_TO_INT (record_type_ptr);
747 : : GHashTableIter record_iter;
748 : : const char *identifier;
749 : : GPtrArray *time_spans_array; /* (element-type MctTimeSpan) */
750 : 0 : g_autoptr(GHashTable) gvdb_record_table = NULL;
751 : :
752 : 0 : g_hash_table_iter_init (&record_iter, record_data);
753 : 0 : gvdb_record_table = gvdb_hash_table_new (gvdb_data_table, mct_timer_store_record_type_to_string (record_type));
754 : :
755 [ # # ]: 0 : while (g_hash_table_iter_next (&record_iter, (void **) &identifier, (void **) &time_spans_array))
756 : : {
757 : : GvdbItem *item;
758 : 0 : g_auto(GVariantBuilder) time_spans_builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("a(tt)"));
759 : :
760 : : /* Ensure the time spans are coalesced and sorted */
761 : 0 : coalesce_and_sort_time_spans (time_spans_array);
762 : :
763 : : /* Expire old entries */
764 [ # # ]: 0 : if (expiry_cutoff_secs > 0)
765 : 0 : trim_expired_time_spans (time_spans_array, expiry_cutoff_secs);
766 : :
767 : : /* Turn them into a GVariant */
768 [ # # ]: 0 : for (unsigned int i = 0; i < time_spans_array->len; i++)
769 : : {
770 : 0 : MctTimeSpan *time_span = g_ptr_array_index (time_spans_array, i);
771 : 0 : g_variant_builder_add (&time_spans_builder, "(tt)",
772 : : mct_time_span_get_start_time_secs (time_span),
773 : : mct_time_span_get_end_time_secs (time_span));
774 : : }
775 : :
776 : 0 : item = gvdb_hash_table_insert (gvdb_record_table, identifier);
777 : 0 : gvdb_item_set_value (item, g_variant_builder_end (&time_spans_builder));
778 : : }
779 : : }
780 : :
781 : : /* Ensure the directory exists */
782 : 0 : database_file = get_database_file_for_username (self, username);
783 : 0 : database_directory_path = g_path_get_dirname (g_file_peek_path (database_file));
784 : :
785 [ # # ]: 0 : if (g_mkdir_with_parents (database_directory_path, 0700) != 0)
786 : : {
787 : 0 : int saved_errno = errno;
788 : 0 : g_debug ("Failed to create directory ‘%s’: %s", database_directory_path,
789 : : g_strerror (saved_errno));
790 : : /* continue anyway and let the error ultimately be reported by the file
791 : : * writing code */
792 : : }
793 : :
794 : : /* Write out the file */
795 : 0 : g_task_set_task_data (task, save_transaction_data_new (username, gvdb_data_table), (GDestroyNotify) save_transaction_data_free);
796 : 0 : gvdb_table_write_contents_async (gvdb_data_table,
797 : 0 : g_file_peek_path (database_file),
798 : : G_BYTE_ORDER != G_LITTLE_ENDIAN,
799 : : cancellable, save_transaction_write_contents_cb,
800 : : g_steal_pointer (&task));
801 : : }
802 : :
803 : : static void
804 : 0 : save_transaction_write_contents_cb (GObject *object,
805 : : GAsyncResult *result,
806 : : void *user_data)
807 : : {
808 : 0 : g_autoptr(GTask) task = g_steal_pointer (&user_data);
809 : 0 : MctTimerStore *self = g_task_get_source_object (task);
810 : 0 : SaveTransactionData *data = g_task_get_task_data (task);
811 : 0 : g_autoptr(GError) local_error = NULL;
812 : :
813 : : /* Finish the write */
814 : 0 : gvdb_table_write_contents_finish (data->gvdb_data_table, result, &local_error);
815 : :
816 [ # # ]: 0 : if (local_error != NULL)
817 : : {
818 : : /* Don’t clear data and don’t emit a signal on error. This means callers
819 : : * will have to call mct_timer_store_roll_back_transaction() before
820 : : * disposing of the TimerStore, if they can’t save again. */
821 : 0 : g_task_return_error (task, g_steal_pointer (&local_error));
822 : : }
823 : : else
824 : : {
825 : : /* Clear the open state from memory now it’s saved to disk. */
826 : 0 : mct_timer_store_roll_back_transaction (self, data->username);
827 : :
828 : : /* Notify of changes. It might be possible to improve the specificity of this
829 : : * by working out whether the additions to the time spans in the database have
830 : : * affected estimated end times. But the likelihood is that they have (users
831 : : * will typically not be adding historic time spans). */
832 : 0 : g_signal_emit (self, signals[SIGNAL_ESTIMATED_END_TIMES_CHANGED], 0, data->username);
833 : :
834 : 0 : g_task_return_boolean (task, TRUE);
835 : : }
836 : 0 : }
837 : :
838 : : /**
839 : : * mct_timer_store_save_transaction_finish:
840 : : * @self: a timer store
841 : : * @result: result of the asynchronous operation
842 : : * @error: return location for a [type@GLib.Error], or `NULL` to ignore
843 : : *
844 : : * Finish an asynchronous operation started with
845 : : * [method@Mct.TimerStore.save_transaction_async].
846 : : *
847 : : * Returns: true if the operation succeeded, false otherwise
848 : : * Since: 0.14.0
849 : : */
850 : : gboolean
851 : 0 : mct_timer_store_save_transaction_finish (MctTimerStore *self,
852 : : GAsyncResult *result,
853 : : GError **error)
854 : : {
855 : 0 : g_return_val_if_fail (MCT_IS_TIMER_STORE (self), FALSE);
856 : 0 : g_return_val_if_fail (g_task_is_valid (result, self), FALSE);
857 : 0 : g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
858 : :
859 : 0 : return g_task_propagate_boolean (G_TASK (result), error);
860 : : }
861 : :
862 : : /**
863 : : * mct_timer_store_roll_back_transaction:
864 : : * @self: a timer store
865 : : * @transaction: an open transaction handle
866 : : *
867 : : * Cancel the given @transaction and discard any pending changes from it.
868 : : *
869 : : * Since: 0.14.0
870 : : */
871 : : void
872 : 2 : mct_timer_store_roll_back_transaction (MctTimerStore *self,
873 : : const MctTimerStoreTransaction *transaction)
874 : : {
875 : : gboolean removed;
876 : :
877 : 2 : g_return_if_fail (MCT_IS_TIMER_STORE (self));
878 : 2 : g_return_if_fail (transaction != NULL);
879 : :
880 : : /* Must already have an open user. Throw away the transaction. */
881 : 2 : removed = g_hash_table_remove (self->open_data, transaction);
882 : 2 : g_return_if_fail (removed);
883 : : }
884 : :
885 : : /**
886 : : * mct_timer_store_add_time_spans:
887 : : * @self: a timer store
888 : : * @transaction: an open transaction handle
889 : : * @record_type: type of record to add
890 : : * @identifier: identifier for the record, must match the format required
891 : : * by @record_type
892 : : * @time_spans: (array length=n_time_spans): zero or more time spans to add to
893 : : * the database
894 : : * @n_time_spans: number of time spans in @time_spans
895 : : *
896 : : * Add time spans to the database file represented by @transaction.
897 : : *
898 : : * This is the way to record session and app time usage by the user whose
899 : : * database file is open as @transaction.
900 : : *
901 : : * Since: 0.14.0
902 : : */
903 : : void
904 : 0 : mct_timer_store_add_time_spans (MctTimerStore *self,
905 : : const MctTimerStoreTransaction *transaction,
906 : : MctTimerStoreRecordType record_type,
907 : : const char *identifier,
908 : : const MctTimeSpan * const *time_spans,
909 : : size_t n_time_spans)
910 : : {
911 : : const char *username;
912 : : GHashTable *open_user_data; /* (element-type MctTimerStoreRecordType, GHashTable<utf8,GPtrArray<MctTimeSpan>>) */
913 : : GHashTable *record_data; /* (element-type utf8 GPtrArray<MctTimeSpan>) */
914 : : GPtrArray *time_spans_array; /* (element-type MctTimeSpan) */
915 : :
916 : 0 : g_return_if_fail (MCT_IS_TIMER_STORE (self));
917 : 0 : g_return_if_fail (transaction != NULL);
918 : 0 : g_return_if_fail (mct_timer_store_record_type_validate_identifier (record_type, identifier, NULL));
919 : 0 : g_return_if_fail (n_time_spans == 0 || time_spans != NULL);
920 : :
921 : : /* Must already have an open user. */
922 : 0 : username = (const char *) transaction;
923 : 0 : open_user_data = g_hash_table_lookup (self->open_data, username);
924 : 0 : g_return_if_fail (open_user_data != NULL);
925 : :
926 : : /* Look up the array for the time spans by (record_type, identifier) */
927 : 0 : record_data = g_hash_table_lookup (open_user_data, GINT_TO_POINTER (record_type));
928 : :
929 [ # # ]: 0 : if (record_data == NULL)
930 : : {
931 : 0 : g_autoptr(GHashTable) record_data_owned = NULL; /* (element-type utf8 GPtrArray<MctTimeSpan>) */
932 : :
933 : 0 : record_data = record_data_owned = g_hash_table_new_full (g_str_hash, g_str_equal,
934 : : g_free, (GDestroyNotify) g_ptr_array_unref);
935 : 0 : g_hash_table_insert (open_user_data, GINT_TO_POINTER (record_type), g_steal_pointer (&record_data_owned));
936 : : }
937 : :
938 : 0 : time_spans_array = g_hash_table_lookup (record_data, identifier);
939 [ # # ]: 0 : if (time_spans_array == NULL)
940 : : {
941 : 0 : g_autoptr(GPtrArray) time_spans_array_owned = NULL; /* (element-type MctTimeSpan) */
942 : 0 : time_spans_array = time_spans_array_owned = g_ptr_array_new_full (n_time_spans, (GDestroyNotify) mct_time_span_free);
943 : 0 : g_hash_table_insert (record_data, g_strdup (identifier), g_steal_pointer (&time_spans_array_owned));
944 : : }
945 : :
946 : : /* Add the time spans. Don’t bother sorting for now; that will happen when saving. */
947 [ # # ]: 0 : for (size_t i = 0; i < n_time_spans; i++)
948 : 0 : g_ptr_array_add (time_spans_array, mct_time_span_copy (time_spans[i]));
949 : : }
950 : :
951 : : /**
952 : : * mct_timer_store_calculate_total_times_between:
953 : : * @self: a timer store
954 : : * @transaction: an open transaction handle
955 : : * @record_type: type of record to query
956 : : * @since_secs: lower limit (inclusive) on the period to calculate times
957 : : * between, in seconds since the Unix epoch
958 : : * @until_secs: upper limit (inclusive) on the period to calculate times
959 : : * between, in seconds since the Unix epoch
960 : : *
961 : : * Calculates times the user has spent on all identifiers for the given
962 : : * @record_type between @since_secs and @until_secs.
963 : : *
964 : : * This is the main method for querying the user’s usage history. It returns a
965 : : * map from identifier to total usage time, in seconds, for each identifier of
966 : : * the given @record_type. The returned map may be empty if there are no
967 : : * records.
968 : : *
969 : : * The calculation is limited to the period [@since_secs, @until_secs] (in
970 : : * [ISO 31-11 notation](https://en.wikipedia.org/wiki/Interval_(mathematics)#Including_or_excluding_endpoints)),
971 : : * with records which cross either of those limits being truncated to the limit.
972 : : * To query for all time, pass `0` for @since_secs and `UINT64_MAX` for
973 : : * @until_secs.
974 : : *
975 : : * @transaction must refer to a valid transaction started with
976 : : * [method@Malcontent.TimerStore.open_username_async]. If you don’t plan to
977 : : * write changes to the user’s database file after calculating times, call
978 : : * [method@Malcontent.TimerStore.roll_back_transaction] afterwards.
979 : : *
980 : : * Returns: (transfer full) (element-type utf8 uint64_t): potentially empty map
981 : : * from identifier to total usage time (in seconds)
982 : : * Since: 0.14.0
983 : : */
984 : : GHashTable *
985 : 0 : mct_timer_store_calculate_total_times_between (MctTimerStore *self,
986 : : const MctTimerStoreTransaction *transaction,
987 : : MctTimerStoreRecordType record_type,
988 : : uint64_t since_secs,
989 : : uint64_t until_secs)
990 : : {
991 : : const char *username;
992 : : GHashTable *open_user_data; /* (element-type MctTimerStoreRecordType, GHashTable<utf8,GPtrArray<MctTimeSpan>>) */
993 : 0 : g_autoptr(GHashTable) total_times = NULL; /* (element-type utf8 uint64_t) */
994 : : GHashTable *record_data; /* (element-type utf8 GPtrArray<MctTimeSpan>) */
995 : : GHashTableIter record_iter;
996 : : const char *identifier;
997 : : GPtrArray *time_spans_array; /* (element-type MctTimeSpan) */
998 : :
999 : 0 : g_return_val_if_fail (MCT_IS_TIMER_STORE (self), NULL);
1000 : 0 : g_return_val_if_fail (transaction != NULL, NULL);
1001 : 0 : g_return_val_if_fail (since_secs < until_secs, NULL);
1002 : :
1003 : : /* Must already have an open user. */
1004 : 0 : username = (const char *) transaction;
1005 : 0 : open_user_data = g_hash_table_lookup (self->open_data, username);
1006 : 0 : g_return_val_if_fail (open_user_data != NULL, NULL);
1007 : :
1008 : 0 : total_times = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
1009 : :
1010 : : /* Look up the record type */
1011 : 0 : record_data = g_hash_table_lookup (open_user_data, GINT_TO_POINTER (record_type));
1012 : :
1013 [ # # ]: 0 : if (record_data == NULL)
1014 : 0 : return g_steal_pointer (&total_times);
1015 : :
1016 : : /* For each identifier and set of time spans, sum up the time spans in the requested range */
1017 : 0 : g_hash_table_iter_init (&record_iter, record_data);
1018 : :
1019 [ # # ]: 0 : while (g_hash_table_iter_next (&record_iter, (void **) &identifier, (void **) &time_spans_array))
1020 : : {
1021 : 0 : uint64_t identifier_total_time = 0;
1022 : 0 : unsigned int sum_range_left = 0, sum_range_right_exclusive = time_spans_array->len;
1023 : :
1024 : : /* Binary searches to find the first time span which includes @since_secs,
1025 : : * and the last time span which includes @until_secs. */
1026 : 0 : for (unsigned int left = 0, right = time_spans_array->len;
1027 [ # # ]: 0 : left < right;
1028 : : /* conditional increment inside loop */)
1029 : : {
1030 : 0 : unsigned int middle = (left + right) / 2;
1031 : 0 : const MctTimeSpan *middle_time_span = g_ptr_array_index (time_spans_array, middle);
1032 : :
1033 [ # # ]: 0 : if (mct_time_span_get_end_time_secs (middle_time_span) < since_secs)
1034 : 0 : left = middle + 1;
1035 : : else
1036 : 0 : right = middle;
1037 : :
1038 : 0 : sum_range_left = left;
1039 : : }
1040 : :
1041 : 0 : for (unsigned int left = 0, right = time_spans_array->len;
1042 [ # # ]: 0 : left < right;
1043 : : /* conditional increment inside loop */)
1044 : : {
1045 : 0 : unsigned int middle = (left + right) / 2;
1046 : 0 : const MctTimeSpan *middle_time_span = g_ptr_array_index (time_spans_array, middle);
1047 : :
1048 [ # # ]: 0 : if (mct_time_span_get_start_time_secs (middle_time_span) > until_secs)
1049 : 0 : right = middle;
1050 : : else
1051 : 0 : left = middle + 1;
1052 : :
1053 : 0 : sum_range_right_exclusive = right;
1054 : : }
1055 : :
1056 : : /* Sum the spans using saturation arithmetic in case of overflow */
1057 [ # # ]: 0 : for (unsigned int i = sum_range_left; i < sum_range_right_exclusive; i++)
1058 : : {
1059 : 0 : const MctTimeSpan *time_span = g_ptr_array_index (time_spans_array, i);
1060 : : uint64_t start_secs, end_secs;
1061 : :
1062 [ # # ]: 0 : start_secs = MAX (since_secs, mct_time_span_get_start_time_secs (time_span));
1063 [ # # ]: 0 : end_secs = MIN (mct_time_span_get_end_time_secs (time_span), until_secs);
1064 : 0 : g_assert (start_secs <= end_secs);
1065 : :
1066 [ # # ]: 0 : if (!g_uint64_checked_add (&identifier_total_time,
1067 : : identifier_total_time,
1068 : : end_secs - start_secs))
1069 : : {
1070 : 0 : identifier_total_time = G_MAXUINT64;
1071 : 0 : break;
1072 : : }
1073 : : }
1074 : :
1075 [ # # ]: 0 : if (identifier_total_time > 0)
1076 : 0 : g_hash_table_insert (total_times,
1077 : 0 : g_strdup (identifier),
1078 : : g_memdup2 (&identifier_total_time, sizeof (identifier_total_time)));
1079 : : }
1080 : :
1081 : 0 : return g_steal_pointer (&total_times);
1082 : : }
|