LCOV - code coverage report
Current view: top level - libmalcontent-timer - timer-store.c (source / functions) Coverage Total Hit
Test: 2 coverage DB files Lines: 30.1 % 309 93
Test Date: 2025-04-24 23:46:12 Functions: 46.9 % 32 15
Branches: 19.6 % 158 31

             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                 :             : }
        

Generated by: LCOV version 2.0-1