LCOV - code coverage report
Current view: top level - src/backend/storage/large_object - inv_api.c (source / functions) Coverage Total Hit
Test: Code coverage Lines: 95.2 % 399 380
Test Date: 2026-01-26 10:56:24 Functions: 100.0 % 13 13
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 49.1 % 281 138

             Branch data     Line data    Source code
       1                 :             : /*-------------------------------------------------------------------------
       2                 :             :  *
       3                 :             :  * inv_api.c
       4                 :             :  *        routines for manipulating inversion fs large objects. This file
       5                 :             :  *        contains the user-level large object application interface routines.
       6                 :             :  *
       7                 :             :  *
       8                 :             :  * Note: we access pg_largeobject.data using its C struct declaration.
       9                 :             :  * This is safe because it immediately follows pageno which is an int4 field,
      10                 :             :  * and therefore the data field will always be 4-byte aligned, even if it
      11                 :             :  * is in the short 1-byte-header format.  We have to detoast it since it's
      12                 :             :  * quite likely to be in compressed or short format.  We also need to check
      13                 :             :  * for NULLs, since initdb will mark loid and pageno but not data as NOT NULL.
      14                 :             :  *
      15                 :             :  * Note: many of these routines leak memory in CurrentMemoryContext, as indeed
      16                 :             :  * does most of the backend code.  We expect that CurrentMemoryContext will
      17                 :             :  * be a short-lived context.  Data that must persist across function calls
      18                 :             :  * is kept either in CacheMemoryContext (the Relation structs) or in the
      19                 :             :  * memory context given to inv_open (for LargeObjectDesc structs).
      20                 :             :  *
      21                 :             :  *
      22                 :             :  * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
      23                 :             :  * Portions Copyright (c) 1994, Regents of the University of California
      24                 :             :  *
      25                 :             :  *
      26                 :             :  * IDENTIFICATION
      27                 :             :  *        src/backend/storage/large_object/inv_api.c
      28                 :             :  *
      29                 :             :  *-------------------------------------------------------------------------
      30                 :             :  */
      31                 :             : #include "postgres.h"
      32                 :             : 
      33                 :             : #include <limits.h>
      34                 :             : 
      35                 :             : #include "access/detoast.h"
      36                 :             : #include "access/genam.h"
      37                 :             : #include "access/htup_details.h"
      38                 :             : #include "access/table.h"
      39                 :             : #include "access/xact.h"
      40                 :             : #include "catalog/dependency.h"
      41                 :             : #include "catalog/indexing.h"
      42                 :             : #include "catalog/objectaccess.h"
      43                 :             : #include "catalog/pg_largeobject.h"
      44                 :             : #include "libpq/libpq-fs.h"
      45                 :             : #include "miscadmin.h"
      46                 :             : #include "storage/large_object.h"
      47                 :             : #include "utils/acl.h"
      48                 :             : #include "utils/fmgroids.h"
      49                 :             : #include "utils/rel.h"
      50                 :             : #include "utils/snapmgr.h"
      51                 :             : 
      52                 :             : 
      53                 :             : /*
      54                 :             :  * GUC: backwards-compatibility flag to suppress LO permission checks
      55                 :             :  */
      56                 :             : bool            lo_compat_privileges;
      57                 :             : 
      58                 :             : /*
      59                 :             :  * All accesses to pg_largeobject and its index make use of a single
      60                 :             :  * Relation reference.  To guarantee that the relcache entry remains
      61                 :             :  * in the cache, on the first reference inside a subtransaction, we
      62                 :             :  * execute a slightly klugy maneuver to assign ownership of the
      63                 :             :  * Relation reference to TopTransactionResourceOwner.
      64                 :             :  */
      65                 :             : static Relation lo_heap_r = NULL;
      66                 :             : static Relation lo_index_r = NULL;
      67                 :             : 
      68                 :             : 
      69                 :             : /*
      70                 :             :  * Open pg_largeobject and its index, if not already done in current xact
      71                 :             :  */
      72                 :             : static void
      73                 :         470 : open_lo_relation(void)
      74                 :             : {
      75                 :         470 :         ResourceOwner currentOwner;
      76                 :             : 
      77   [ +  +  -  + ]:         470 :         if (lo_heap_r && lo_index_r)
      78                 :         436 :                 return;                                 /* already open in current xact */
      79                 :             : 
      80                 :             :         /* Arrange for the top xact to own these relation references */
      81                 :          34 :         currentOwner = CurrentResourceOwner;
      82                 :          34 :         CurrentResourceOwner = TopTransactionResourceOwner;
      83                 :             : 
      84                 :             :         /* Use RowExclusiveLock since we might either read or write */
      85         [ -  + ]:          34 :         if (lo_heap_r == NULL)
      86                 :          34 :                 lo_heap_r = table_open(LargeObjectRelationId, RowExclusiveLock);
      87         [ -  + ]:          34 :         if (lo_index_r == NULL)
      88                 :          34 :                 lo_index_r = index_open(LargeObjectLOidPNIndexId, RowExclusiveLock);
      89                 :             : 
      90                 :          34 :         CurrentResourceOwner = currentOwner;
      91         [ -  + ]:         470 : }
      92                 :             : 
      93                 :             : /*
      94                 :             :  * Clean up at main transaction end
      95                 :             :  */
      96                 :             : void
      97                 :          58 : close_lo_relation(bool isCommit)
      98                 :             : {
      99   [ +  +  +  - ]:          58 :         if (lo_heap_r || lo_index_r)
     100                 :             :         {
     101                 :             :                 /*
     102                 :             :                  * Only bother to close if committing; else abort cleanup will handle
     103                 :             :                  * it
     104                 :             :                  */
     105         [ +  + ]:          34 :                 if (isCommit)
     106                 :             :                 {
     107                 :          33 :                         ResourceOwner currentOwner;
     108                 :             : 
     109                 :          33 :                         currentOwner = CurrentResourceOwner;
     110                 :          33 :                         CurrentResourceOwner = TopTransactionResourceOwner;
     111                 :             : 
     112         [ -  + ]:          33 :                         if (lo_index_r)
     113                 :          33 :                                 index_close(lo_index_r, NoLock);
     114         [ -  + ]:          33 :                         if (lo_heap_r)
     115                 :          33 :                                 table_close(lo_heap_r, NoLock);
     116                 :             : 
     117                 :          33 :                         CurrentResourceOwner = currentOwner;
     118                 :          33 :                 }
     119                 :          34 :                 lo_heap_r = NULL;
     120                 :          34 :                 lo_index_r = NULL;
     121                 :          34 :         }
     122                 :          58 : }
     123                 :             : 
     124                 :             : 
     125                 :             : /*
     126                 :             :  * Extract data field from a pg_largeobject tuple, detoasting if needed
     127                 :             :  * and verifying that the length is sane.  Returns data pointer (a bytea *),
     128                 :             :  * data length, and an indication of whether to pfree the data pointer.
     129                 :             :  */
     130                 :             : static void
     131                 :        1680 : getdatafield(Form_pg_largeobject tuple,
     132                 :             :                          bytea **pdatafield,
     133                 :             :                          int *plen,
     134                 :             :                          bool *pfreeit)
     135                 :             : {
     136                 :        1680 :         bytea      *datafield;
     137                 :        1680 :         int                     len;
     138                 :        1680 :         bool            freeit;
     139                 :             : 
     140                 :        1680 :         datafield = &(tuple->data); /* see note at top of file */
     141                 :        1680 :         freeit = false;
     142         [ +  + ]:        1680 :         if (VARATT_IS_EXTENDED(datafield))
     143                 :             :         {
     144                 :        1655 :                 datafield = (bytea *)
     145                 :        1655 :                         detoast_attr((struct varlena *) datafield);
     146                 :        1655 :                 freeit = true;
     147                 :        1655 :         }
     148                 :        1680 :         len = VARSIZE(datafield) - VARHDRSZ;
     149         [ +  - ]:        1680 :         if (len < 0 || len > LOBLKSIZE)
     150   [ #  #  #  # ]:           0 :                 ereport(ERROR,
     151                 :             :                                 (errcode(ERRCODE_DATA_CORRUPTED),
     152                 :             :                                  errmsg("pg_largeobject entry for OID %u, page %d has invalid data field size %d",
     153                 :             :                                                 tuple->loid, tuple->pageno, len)));
     154                 :        1680 :         *pdatafield = datafield;
     155                 :        1680 :         *plen = len;
     156                 :        1680 :         *pfreeit = freeit;
     157                 :        1680 : }
     158                 :             : 
     159                 :             : 
     160                 :             : /*
     161                 :             :  *      inv_create -- create a new large object
     162                 :             :  *
     163                 :             :  *      Arguments:
     164                 :             :  *        lobjId - OID to use for new large object, or InvalidOid to pick one
     165                 :             :  *
     166                 :             :  *      Returns:
     167                 :             :  *        OID of new object
     168                 :             :  *
     169                 :             :  * If lobjId is not InvalidOid, then an error occurs if the OID is already
     170                 :             :  * in use.
     171                 :             :  */
     172                 :             : Oid
     173                 :          20 : inv_create(Oid lobjId)
     174                 :             : {
     175                 :          20 :         Oid                     lobjId_new;
     176                 :             : 
     177                 :             :         /*
     178                 :             :          * Create a new largeobject with empty data pages
     179                 :             :          */
     180                 :          20 :         lobjId_new = LargeObjectCreate(lobjId);
     181                 :             : 
     182                 :             :         /*
     183                 :             :          * dependency on the owner of largeobject
     184                 :             :          *
     185                 :             :          * Note that LO dependencies are recorded using classId
     186                 :             :          * LargeObjectRelationId for backwards-compatibility reasons.  Using
     187                 :             :          * LargeObjectMetadataRelationId instead would simplify matters for the
     188                 :             :          * backend, but it'd complicate pg_dump and possibly break other clients.
     189                 :             :          */
     190                 :          20 :         recordDependencyOnOwner(LargeObjectRelationId,
     191                 :          20 :                                                         lobjId_new, GetUserId());
     192                 :             : 
     193                 :             :         /* Post creation hook for new large object */
     194         [ +  - ]:          20 :         InvokeObjectPostCreateHook(LargeObjectRelationId, lobjId_new, 0);
     195                 :             : 
     196                 :             :         /*
     197                 :             :          * Advance command counter to make new tuple visible to later operations.
     198                 :             :          */
     199                 :          20 :         CommandCounterIncrement();
     200                 :             : 
     201                 :          40 :         return lobjId_new;
     202                 :          20 : }
     203                 :             : 
     204                 :             : /*
     205                 :             :  *      inv_open -- access an existing large object.
     206                 :             :  *
     207                 :             :  * Returns a large object descriptor, appropriately filled in.
     208                 :             :  * The descriptor and subsidiary data are allocated in the specified
     209                 :             :  * memory context, which must be suitably long-lived for the caller's
     210                 :             :  * purposes.  If the returned descriptor has a snapshot associated
     211                 :             :  * with it, the caller must ensure that it also lives long enough,
     212                 :             :  * e.g. by calling RegisterSnapshotOnOwner
     213                 :             :  */
     214                 :             : LargeObjectDesc *
     215                 :          50 : inv_open(Oid lobjId, int flags, MemoryContext mcxt)
     216                 :             : {
     217                 :          50 :         LargeObjectDesc *retval;
     218                 :          50 :         Snapshot        snapshot = NULL;
     219                 :          50 :         int                     descflags = 0;
     220                 :             : 
     221                 :             :         /*
     222                 :             :          * Historically, no difference is made between (INV_WRITE) and (INV_WRITE
     223                 :             :          * | INV_READ), the caller being allowed to read the large object
     224                 :             :          * descriptor in either case.
     225                 :             :          */
     226         [ +  + ]:          50 :         if (flags & INV_WRITE)
     227                 :          25 :                 descflags |= IFS_WRLOCK | IFS_RDLOCK;
     228         [ +  + ]:          50 :         if (flags & INV_READ)
     229                 :          30 :                 descflags |= IFS_RDLOCK;
     230                 :             : 
     231         [ +  - ]:          50 :         if (descflags == 0)
     232   [ #  #  #  # ]:           0 :                 ereport(ERROR,
     233                 :             :                                 (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
     234                 :             :                                  errmsg("invalid flags for opening a large object: %d",
     235                 :             :                                                 flags)));
     236                 :             : 
     237                 :             :         /* Get snapshot.  If write is requested, use an instantaneous snapshot. */
     238         [ +  + ]:          50 :         if (descflags & IFS_WRLOCK)
     239                 :          25 :                 snapshot = NULL;
     240                 :             :         else
     241                 :          25 :                 snapshot = GetActiveSnapshot();
     242                 :             : 
     243                 :             :         /* Can't use LargeObjectExists here because we need to specify snapshot */
     244         [ +  - ]:          50 :         if (!LargeObjectExistsWithSnapshot(lobjId, snapshot))
     245   [ #  #  #  # ]:           0 :                 ereport(ERROR,
     246                 :             :                                 (errcode(ERRCODE_UNDEFINED_OBJECT),
     247                 :             :                                  errmsg("large object %u does not exist", lobjId)));
     248                 :             : 
     249                 :             :         /* Apply permission checks, again specifying snapshot */
     250         [ -  + ]:          50 :         if ((descflags & IFS_RDLOCK) != 0)
     251                 :             :         {
     252   [ +  +  +  + ]:          50 :                 if (!lo_compat_privileges &&
     253                 :          94 :                         pg_largeobject_aclcheck_snapshot(lobjId,
     254                 :          47 :                                                                                          GetUserId(),
     255                 :             :                                                                                          ACL_SELECT,
     256                 :          94 :                                                                                          snapshot) != ACLCHECK_OK)
     257   [ +  -  +  - ]:           7 :                         ereport(ERROR,
     258                 :             :                                         (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
     259                 :             :                                          errmsg("permission denied for large object %u",
     260                 :             :                                                         lobjId)));
     261                 :          43 :         }
     262         [ +  + ]:          43 :         if ((descflags & IFS_WRLOCK) != 0)
     263                 :             :         {
     264   [ +  +  +  + ]:          21 :                 if (!lo_compat_privileges &&
     265                 :          38 :                         pg_largeobject_aclcheck_snapshot(lobjId,
     266                 :          19 :                                                                                          GetUserId(),
     267                 :             :                                                                                          ACL_UPDATE,
     268                 :          38 :                                                                                          snapshot) != ACLCHECK_OK)
     269   [ +  -  +  - ]:           2 :                         ereport(ERROR,
     270                 :             :                                         (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
     271                 :             :                                          errmsg("permission denied for large object %u",
     272                 :             :                                                         lobjId)));
     273                 :          19 :         }
     274                 :             : 
     275                 :             :         /* OK to create a descriptor */
     276                 :          41 :         retval = (LargeObjectDesc *) MemoryContextAlloc(mcxt,
     277                 :             :                                                                                                         sizeof(LargeObjectDesc));
     278                 :          41 :         retval->id = lobjId;
     279                 :          41 :         retval->offset = 0;
     280                 :          41 :         retval->flags = descflags;
     281                 :             : 
     282                 :             :         /* caller sets if needed, not used by the functions in this file */
     283                 :          41 :         retval->subid = InvalidSubTransactionId;
     284                 :             : 
     285                 :             :         /*
     286                 :             :          * The snapshot (if any) is just the currently active snapshot.  The
     287                 :             :          * caller will replace it with a longer-lived copy if needed.
     288                 :             :          */
     289                 :          41 :         retval->snapshot = snapshot;
     290                 :             : 
     291                 :          82 :         return retval;
     292                 :          41 : }
     293                 :             : 
     294                 :             : /*
     295                 :             :  * Closes a large object descriptor previously made by inv_open(), and
     296                 :             :  * releases the long-term memory used by it.
     297                 :             :  */
     298                 :             : void
     299                 :          36 : inv_close(LargeObjectDesc *obj_desc)
     300                 :             : {
     301         [ +  - ]:          36 :         Assert(obj_desc);
     302                 :          36 :         pfree(obj_desc);
     303                 :          36 : }
     304                 :             : 
     305                 :             : /*
     306                 :             :  * Destroys an existing large object (not to be confused with a descriptor!)
     307                 :             :  *
     308                 :             :  * Note we expect caller to have done any required permissions check.
     309                 :             :  */
     310                 :             : int
     311                 :          13 : inv_drop(Oid lobjId)
     312                 :             : {
     313                 :          13 :         ObjectAddress object;
     314                 :             : 
     315                 :             :         /*
     316                 :             :          * Delete any comments and dependencies on the large object
     317                 :             :          */
     318                 :          13 :         object.classId = LargeObjectRelationId;
     319                 :          13 :         object.objectId = lobjId;
     320                 :          13 :         object.objectSubId = 0;
     321                 :          13 :         performDeletion(&object, DROP_CASCADE, 0);
     322                 :             : 
     323                 :             :         /*
     324                 :             :          * Advance command counter so that tuple removal will be seen by later
     325                 :             :          * large-object operations in this transaction.
     326                 :             :          */
     327                 :          13 :         CommandCounterIncrement();
     328                 :             : 
     329                 :             :         /* For historical reasons, we always return 1 on success. */
     330                 :          13 :         return 1;
     331                 :          13 : }
     332                 :             : 
     333                 :             : /*
     334                 :             :  * Determine size of a large object
     335                 :             :  *
     336                 :             :  * NOTE: LOs can contain gaps, just like Unix files.  We actually return
     337                 :             :  * the offset of the last byte + 1.
     338                 :             :  */
     339                 :             : static uint64
     340                 :          16 : inv_getsize(LargeObjectDesc *obj_desc)
     341                 :             : {
     342                 :          16 :         uint64          lastbyte = 0;
     343                 :          16 :         ScanKeyData skey[1];
     344                 :          16 :         SysScanDesc sd;
     345                 :          16 :         HeapTuple       tuple;
     346                 :             : 
     347         [ +  - ]:          16 :         Assert(obj_desc);
     348                 :             : 
     349                 :          16 :         open_lo_relation();
     350                 :             : 
     351                 :          32 :         ScanKeyInit(&skey[0],
     352                 :             :                                 Anum_pg_largeobject_loid,
     353                 :             :                                 BTEqualStrategyNumber, F_OIDEQ,
     354                 :          16 :                                 ObjectIdGetDatum(obj_desc->id));
     355                 :             : 
     356                 :          32 :         sd = systable_beginscan_ordered(lo_heap_r, lo_index_r,
     357                 :          16 :                                                                         obj_desc->snapshot, 1, skey);
     358                 :             : 
     359                 :             :         /*
     360                 :             :          * Because the pg_largeobject index is on both loid and pageno, but we
     361                 :             :          * constrain only loid, a backwards scan should visit all pages of the
     362                 :             :          * large object in reverse pageno order.  So, it's sufficient to examine
     363                 :             :          * the first valid tuple (== last valid page).
     364                 :             :          */
     365                 :          16 :         tuple = systable_getnext_ordered(sd, BackwardScanDirection);
     366         [ -  + ]:          16 :         if (HeapTupleIsValid(tuple))
     367                 :             :         {
     368                 :          16 :                 Form_pg_largeobject data;
     369                 :          16 :                 bytea      *datafield;
     370                 :          16 :                 int                     len;
     371                 :          16 :                 bool            pfreeit;
     372                 :             : 
     373         [ +  - ]:          16 :                 if (HeapTupleHasNulls(tuple))   /* paranoia */
     374   [ #  #  #  # ]:           0 :                         elog(ERROR, "null field found in pg_largeobject");
     375                 :          16 :                 data = (Form_pg_largeobject) GETSTRUCT(tuple);
     376                 :          16 :                 getdatafield(data, &datafield, &len, &pfreeit);
     377                 :          16 :                 lastbyte = (uint64) data->pageno * LOBLKSIZE + len;
     378         [ +  + ]:          16 :                 if (pfreeit)
     379                 :           3 :                         pfree(datafield);
     380                 :          16 :         }
     381                 :             : 
     382                 :          16 :         systable_endscan_ordered(sd);
     383                 :             : 
     384                 :          32 :         return lastbyte;
     385                 :          16 : }
     386                 :             : 
     387                 :             : int64
     388                 :          34 : inv_seek(LargeObjectDesc *obj_desc, int64 offset, int whence)
     389                 :             : {
     390                 :          34 :         int64           newoffset;
     391                 :             : 
     392         [ +  - ]:          34 :         Assert(obj_desc);
     393                 :             : 
     394                 :             :         /*
     395                 :             :          * We allow seek/tell if you have either read or write permission, so no
     396                 :             :          * need for a permission check here.
     397                 :             :          */
     398                 :             : 
     399                 :             :         /*
     400                 :             :          * Note: overflow in the additions is possible, but since we will reject
     401                 :             :          * negative results, we don't need any extra test for that.
     402                 :             :          */
     403   [ +  +  +  - ]:          34 :         switch (whence)
     404                 :             :         {
     405                 :             :                 case SEEK_SET:
     406                 :          15 :                         newoffset = offset;
     407                 :          15 :                         break;
     408                 :             :                 case SEEK_CUR:
     409                 :           3 :                         newoffset = obj_desc->offset + offset;
     410                 :           3 :                         break;
     411                 :             :                 case SEEK_END:
     412                 :          16 :                         newoffset = inv_getsize(obj_desc) + offset;
     413                 :          16 :                         break;
     414                 :             :                 default:
     415   [ #  #  #  # ]:           0 :                         ereport(ERROR,
     416                 :             :                                         (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
     417                 :             :                                          errmsg("invalid whence setting: %d", whence)));
     418                 :           0 :                         newoffset = 0;          /* keep compiler quiet */
     419                 :           0 :                         break;
     420                 :             :         }
     421                 :             : 
     422                 :             :         /*
     423                 :             :          * use errmsg_internal here because we don't want to expose INT64_FORMAT
     424                 :             :          * in translatable strings; doing better is not worth the trouble
     425                 :             :          */
     426         [ +  - ]:          34 :         if (newoffset < 0 || newoffset > MAX_LARGE_OBJECT_SIZE)
     427   [ #  #  #  # ]:           0 :                 ereport(ERROR,
     428                 :             :                                 (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
     429                 :             :                                  errmsg_internal("invalid large object seek target: " INT64_FORMAT,
     430                 :             :                                                                  newoffset)));
     431                 :             : 
     432                 :          34 :         obj_desc->offset = newoffset;
     433                 :          68 :         return newoffset;
     434                 :          34 : }
     435                 :             : 
     436                 :             : int64
     437                 :           8 : inv_tell(LargeObjectDesc *obj_desc)
     438                 :             : {
     439         [ +  - ]:           8 :         Assert(obj_desc);
     440                 :             : 
     441                 :             :         /*
     442                 :             :          * We allow seek/tell if you have either read or write permission, so no
     443                 :             :          * need for a permission check here.
     444                 :             :          */
     445                 :             : 
     446                 :           8 :         return obj_desc->offset;
     447                 :             : }
     448                 :             : 
     449                 :             : int
     450                 :         189 : inv_read(LargeObjectDesc *obj_desc, char *buf, int nbytes)
     451                 :             : {
     452                 :         189 :         int                     nread = 0;
     453                 :         189 :         int64           n;
     454                 :         189 :         int64           off;
     455                 :         189 :         int                     len;
     456                 :         189 :         int32           pageno = (int32) (obj_desc->offset / LOBLKSIZE);
     457                 :         189 :         uint64          pageoff;
     458                 :         189 :         ScanKeyData skey[2];
     459                 :         189 :         SysScanDesc sd;
     460                 :         189 :         HeapTuple       tuple;
     461                 :             : 
     462         [ +  - ]:         189 :         Assert(obj_desc);
     463         [ +  - ]:         189 :         Assert(buf != NULL);
     464                 :             : 
     465         [ +  - ]:         189 :         if ((obj_desc->flags & IFS_RDLOCK) == 0)
     466   [ #  #  #  # ]:           0 :                 ereport(ERROR,
     467                 :             :                                 (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
     468                 :             :                                  errmsg("permission denied for large object %u",
     469                 :             :                                                 obj_desc->id)));
     470                 :             : 
     471         [ -  + ]:         189 :         if (nbytes <= 0)
     472                 :           0 :                 return 0;
     473                 :             : 
     474                 :         189 :         open_lo_relation();
     475                 :             : 
     476                 :         378 :         ScanKeyInit(&skey[0],
     477                 :             :                                 Anum_pg_largeobject_loid,
     478                 :             :                                 BTEqualStrategyNumber, F_OIDEQ,
     479                 :         189 :                                 ObjectIdGetDatum(obj_desc->id));
     480                 :             : 
     481                 :         378 :         ScanKeyInit(&skey[1],
     482                 :             :                                 Anum_pg_largeobject_pageno,
     483                 :             :                                 BTGreaterEqualStrategyNumber, F_INT4GE,
     484                 :         189 :                                 Int32GetDatum(pageno));
     485                 :             : 
     486                 :         378 :         sd = systable_beginscan_ordered(lo_heap_r, lo_index_r,
     487                 :         189 :                                                                         obj_desc->snapshot, 2, skey);
     488                 :             : 
     489         [ +  + ]:        1671 :         while ((tuple = systable_getnext_ordered(sd, ForwardScanDirection)) != NULL)
     490                 :             :         {
     491                 :        1659 :                 Form_pg_largeobject data;
     492                 :        1659 :                 bytea      *datafield;
     493                 :        1659 :                 bool            pfreeit;
     494                 :             : 
     495         [ +  - ]:        1659 :                 if (HeapTupleHasNulls(tuple))   /* paranoia */
     496   [ #  #  #  # ]:           0 :                         elog(ERROR, "null field found in pg_largeobject");
     497                 :        1659 :                 data = (Form_pg_largeobject) GETSTRUCT(tuple);
     498                 :             : 
     499                 :             :                 /*
     500                 :             :                  * We expect the indexscan will deliver pages in order.  However,
     501                 :             :                  * there may be missing pages if the LO contains unwritten "holes". We
     502                 :             :                  * want missing sections to read out as zeroes.
     503                 :             :                  */
     504                 :        1659 :                 pageoff = ((uint64) data->pageno) * LOBLKSIZE;
     505         [ +  + ]:        1659 :                 if (pageoff > obj_desc->offset)
     506                 :             :                 {
     507                 :           2 :                         n = pageoff - obj_desc->offset;
     508         [ +  + ]:           2 :                         n = (n <= (nbytes - nread)) ? n : (nbytes - nread);
     509   [ -  +  #  #  :           2 :                         MemSet(buf + nread, 0, n);
          #  #  #  #  #  
                      # ]
     510                 :           2 :                         nread += n;
     511                 :           2 :                         obj_desc->offset += n;
     512                 :           2 :                 }
     513                 :             : 
     514         [ +  + ]:        1659 :                 if (nread < nbytes)
     515                 :             :                 {
     516         [ +  - ]:        1658 :                         Assert(obj_desc->offset >= pageoff);
     517                 :        1658 :                         off = (int) (obj_desc->offset - pageoff);
     518         [ +  - ]:        1658 :                         Assert(off >= 0 && off < LOBLKSIZE);
     519                 :             : 
     520                 :        1658 :                         getdatafield(data, &datafield, &len, &pfreeit);
     521         [ +  + ]:        1658 :                         if (len > off)
     522                 :             :                         {
     523                 :        1655 :                                 n = len - off;
     524         [ +  + ]:        1655 :                                 n = (n <= (nbytes - nread)) ? n : (nbytes - nread);
     525                 :        1655 :                                 memcpy(buf + nread, VARDATA(datafield) + off, n);
     526                 :        1655 :                                 nread += n;
     527                 :        1655 :                                 obj_desc->offset += n;
     528                 :        1655 :                         }
     529         [ +  + ]:        1658 :                         if (pfreeit)
     530                 :        1648 :                                 pfree(datafield);
     531                 :        1658 :                 }
     532                 :             : 
     533         [ +  + ]:        1659 :                 if (nread >= nbytes)
     534                 :         177 :                         break;
     535      [ -  +  + ]:        1659 :         }
     536                 :             : 
     537                 :         189 :         systable_endscan_ordered(sd);
     538                 :             : 
     539                 :         189 :         return nread;
     540                 :         189 : }
     541                 :             : 
     542                 :             : int
     543                 :         258 : inv_write(LargeObjectDesc *obj_desc, const char *buf, int nbytes)
     544                 :             : {
     545                 :         258 :         int                     nwritten = 0;
     546                 :         258 :         int                     n;
     547                 :         258 :         int                     off;
     548                 :         258 :         int                     len;
     549                 :         258 :         int32           pageno = (int32) (obj_desc->offset / LOBLKSIZE);
     550                 :         258 :         ScanKeyData skey[2];
     551                 :         258 :         SysScanDesc sd;
     552                 :         258 :         HeapTuple       oldtuple;
     553                 :         258 :         Form_pg_largeobject olddata;
     554                 :         258 :         bool            neednextpage;
     555                 :         258 :         bytea      *datafield;
     556                 :         258 :         bool            pfreeit;
     557                 :         258 :         union
     558                 :             :         {
     559                 :             :                 alignas(int32) bytea hdr;
     560                 :             :                 /* this is to make the union big enough for a LO data chunk: */
     561                 :             :                 char            data[LOBLKSIZE + VARHDRSZ];
     562                 :         258 :         }                       workbuf = {0};
     563                 :         258 :         char       *workb = VARDATA(&workbuf.hdr);
     564                 :         258 :         HeapTuple       newtup;
     565                 :         258 :         Datum           values[Natts_pg_largeobject];
     566                 :         258 :         bool            nulls[Natts_pg_largeobject];
     567                 :         258 :         bool            replace[Natts_pg_largeobject];
     568                 :         258 :         CatalogIndexState indstate;
     569                 :             : 
     570         [ +  - ]:         258 :         Assert(obj_desc);
     571         [ +  - ]:         258 :         Assert(buf != NULL);
     572                 :             : 
     573                 :             :         /* enforce writability because snapshot is probably wrong otherwise */
     574         [ +  - ]:         258 :         if ((obj_desc->flags & IFS_WRLOCK) == 0)
     575   [ #  #  #  # ]:           0 :                 ereport(ERROR,
     576                 :             :                                 (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
     577                 :             :                                  errmsg("permission denied for large object %u",
     578                 :             :                                                 obj_desc->id)));
     579                 :             : 
     580         [ -  + ]:         258 :         if (nbytes <= 0)
     581                 :           0 :                 return 0;
     582                 :             : 
     583                 :             :         /* this addition can't overflow because nbytes is only int32 */
     584         [ +  - ]:         258 :         if ((nbytes + obj_desc->offset) > MAX_LARGE_OBJECT_SIZE)
     585   [ #  #  #  # ]:           0 :                 ereport(ERROR,
     586                 :             :                                 (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
     587                 :             :                                  errmsg("invalid large object write request size: %d",
     588                 :             :                                                 nbytes)));
     589                 :             : 
     590                 :         258 :         open_lo_relation();
     591                 :             : 
     592                 :         258 :         indstate = CatalogOpenIndexes(lo_heap_r);
     593                 :             : 
     594                 :         516 :         ScanKeyInit(&skey[0],
     595                 :             :                                 Anum_pg_largeobject_loid,
     596                 :             :                                 BTEqualStrategyNumber, F_OIDEQ,
     597                 :         258 :                                 ObjectIdGetDatum(obj_desc->id));
     598                 :             : 
     599                 :         516 :         ScanKeyInit(&skey[1],
     600                 :             :                                 Anum_pg_largeobject_pageno,
     601                 :             :                                 BTGreaterEqualStrategyNumber, F_INT4GE,
     602                 :         258 :                                 Int32GetDatum(pageno));
     603                 :             : 
     604                 :         516 :         sd = systable_beginscan_ordered(lo_heap_r, lo_index_r,
     605                 :         258 :                                                                         obj_desc->snapshot, 2, skey);
     606                 :             : 
     607                 :         258 :         oldtuple = NULL;
     608                 :         258 :         olddata = NULL;
     609                 :         258 :         neednextpage = true;
     610                 :             : 
     611         [ +  + ]:        1582 :         while (nwritten < nbytes)
     612                 :             :         {
     613                 :             :                 /*
     614                 :             :                  * If possible, get next pre-existing page of the LO.  We expect the
     615                 :             :                  * indexscan will deliver these in order --- but there may be holes.
     616                 :             :                  */
     617         [ +  + ]:        1324 :                 if (neednextpage)
     618                 :             :                 {
     619         [ +  + ]:         259 :                         if ((oldtuple = systable_getnext_ordered(sd, ForwardScanDirection)) != NULL)
     620                 :             :                         {
     621         [ +  - ]:           4 :                                 if (HeapTupleHasNulls(oldtuple))        /* paranoia */
     622   [ #  #  #  # ]:           0 :                                         elog(ERROR, "null field found in pg_largeobject");
     623                 :           4 :                                 olddata = (Form_pg_largeobject) GETSTRUCT(oldtuple);
     624         [ +  - ]:           4 :                                 Assert(olddata->pageno >= pageno);
     625                 :           4 :                         }
     626                 :         259 :                         neednextpage = false;
     627                 :         259 :                 }
     628                 :             : 
     629                 :             :                 /*
     630                 :             :                  * If we have a pre-existing page, see if it is the page we want to
     631                 :             :                  * write, or a later one.
     632                 :             :                  */
     633   [ +  +  -  + ]:        1324 :                 if (olddata != NULL && olddata->pageno == pageno)
     634                 :             :                 {
     635                 :             :                         /*
     636                 :             :                          * Update an existing page with fresh data.
     637                 :             :                          *
     638                 :             :                          * First, load old data into workbuf
     639                 :             :                          */
     640                 :           4 :                         getdatafield(olddata, &datafield, &len, &pfreeit);
     641                 :           4 :                         memcpy(workb, VARDATA(datafield), len);
     642         [ +  + ]:           4 :                         if (pfreeit)
     643                 :           3 :                                 pfree(datafield);
     644                 :             : 
     645                 :             :                         /*
     646                 :             :                          * Fill any hole
     647                 :             :                          */
     648                 :           4 :                         off = (int) (obj_desc->offset % LOBLKSIZE);
     649         [ +  - ]:           4 :                         if (off > len)
     650   [ #  #  #  #  :           0 :                                 MemSet(workb + len, 0, off - len);
          #  #  #  #  #  
                      # ]
     651                 :             : 
     652                 :             :                         /*
     653                 :             :                          * Insert appropriate portion of new data
     654                 :             :                          */
     655                 :           4 :                         n = LOBLKSIZE - off;
     656         [ +  + ]:           4 :                         n = (n <= (nbytes - nwritten)) ? n : (nbytes - nwritten);
     657                 :           4 :                         memcpy(workb + off, buf + nwritten, n);
     658                 :           4 :                         nwritten += n;
     659                 :           4 :                         obj_desc->offset += n;
     660                 :           4 :                         off += n;
     661                 :             :                         /* compute valid length of new page */
     662         [ +  - ]:           4 :                         len = (len >= off) ? len : off;
     663                 :           4 :                         SET_VARSIZE(&workbuf.hdr, len + VARHDRSZ);
     664                 :             : 
     665                 :             :                         /*
     666                 :             :                          * Form and insert updated tuple
     667                 :             :                          */
     668                 :           4 :                         memset(values, 0, sizeof(values));
     669                 :           4 :                         memset(nulls, false, sizeof(nulls));
     670                 :           4 :                         memset(replace, false, sizeof(replace));
     671                 :           4 :                         values[Anum_pg_largeobject_data - 1] = PointerGetDatum(&workbuf);
     672                 :           4 :                         replace[Anum_pg_largeobject_data - 1] = true;
     673                 :           8 :                         newtup = heap_modify_tuple(oldtuple, RelationGetDescr(lo_heap_r),
     674                 :           4 :                                                                            values, nulls, replace);
     675                 :           8 :                         CatalogTupleUpdateWithInfo(lo_heap_r, &newtup->t_self, newtup,
     676                 :           4 :                                                                            indstate);
     677                 :           4 :                         heap_freetuple(newtup);
     678                 :             : 
     679                 :             :                         /*
     680                 :             :                          * We're done with this old page.
     681                 :             :                          */
     682                 :           4 :                         oldtuple = NULL;
     683                 :           4 :                         olddata = NULL;
     684                 :           4 :                         neednextpage = true;
     685                 :           4 :                 }
     686                 :             :                 else
     687                 :             :                 {
     688                 :             :                         /*
     689                 :             :                          * Write a brand new page.
     690                 :             :                          *
     691                 :             :                          * First, fill any hole
     692                 :             :                          */
     693                 :        1320 :                         off = (int) (obj_desc->offset % LOBLKSIZE);
     694         [ +  + ]:        1320 :                         if (off > 0)
     695   [ +  -  -  +  :           1 :                                 MemSet(workb, 0, off);
          #  #  #  #  #  
                      # ]
     696                 :             : 
     697                 :             :                         /*
     698                 :             :                          * Insert appropriate portion of new data
     699                 :             :                          */
     700                 :        1320 :                         n = LOBLKSIZE - off;
     701         [ +  + ]:        1320 :                         n = (n <= (nbytes - nwritten)) ? n : (nbytes - nwritten);
     702                 :        1320 :                         memcpy(workb + off, buf + nwritten, n);
     703                 :        1320 :                         nwritten += n;
     704                 :        1320 :                         obj_desc->offset += n;
     705                 :             :                         /* compute valid length of new page */
     706                 :        1320 :                         len = off + n;
     707                 :        1320 :                         SET_VARSIZE(&workbuf.hdr, len + VARHDRSZ);
     708                 :             : 
     709                 :             :                         /*
     710                 :             :                          * Form and insert updated tuple
     711                 :             :                          */
     712                 :        1320 :                         memset(values, 0, sizeof(values));
     713                 :        1320 :                         memset(nulls, false, sizeof(nulls));
     714                 :        1320 :                         values[Anum_pg_largeobject_loid - 1] = ObjectIdGetDatum(obj_desc->id);
     715                 :        1320 :                         values[Anum_pg_largeobject_pageno - 1] = Int32GetDatum(pageno);
     716                 :        1320 :                         values[Anum_pg_largeobject_data - 1] = PointerGetDatum(&workbuf);
     717                 :        1320 :                         newtup = heap_form_tuple(lo_heap_r->rd_att, values, nulls);
     718                 :        1320 :                         CatalogTupleInsertWithInfo(lo_heap_r, newtup, indstate);
     719                 :        1320 :                         heap_freetuple(newtup);
     720                 :             :                 }
     721                 :        1324 :                 pageno++;
     722                 :             :         }
     723                 :             : 
     724                 :         258 :         systable_endscan_ordered(sd);
     725                 :             : 
     726                 :         258 :         CatalogCloseIndexes(indstate);
     727                 :             : 
     728                 :             :         /*
     729                 :             :          * Advance command counter so that my tuple updates will be seen by later
     730                 :             :          * large-object operations in this transaction.
     731                 :             :          */
     732                 :         258 :         CommandCounterIncrement();
     733                 :             : 
     734                 :         258 :         return nwritten;
     735                 :         258 : }
     736                 :             : 
     737                 :             : void
     738                 :           7 : inv_truncate(LargeObjectDesc *obj_desc, int64 len)
     739                 :             : {
     740                 :           7 :         int32           pageno = (int32) (len / LOBLKSIZE);
     741                 :           7 :         int32           off;
     742                 :           7 :         ScanKeyData skey[2];
     743                 :           7 :         SysScanDesc sd;
     744                 :           7 :         HeapTuple       oldtuple;
     745                 :           7 :         Form_pg_largeobject olddata;
     746                 :           7 :         union
     747                 :             :         {
     748                 :             :                 alignas(int32) bytea hdr;
     749                 :             :                 /* this is to make the union big enough for a LO data chunk: */
     750                 :             :                 char            data[LOBLKSIZE + VARHDRSZ];
     751                 :           7 :         }                       workbuf = {0};
     752                 :           7 :         char       *workb = VARDATA(&workbuf.hdr);
     753                 :           7 :         HeapTuple       newtup;
     754                 :           7 :         Datum           values[Natts_pg_largeobject];
     755                 :           7 :         bool            nulls[Natts_pg_largeobject];
     756                 :           7 :         bool            replace[Natts_pg_largeobject];
     757                 :           7 :         CatalogIndexState indstate;
     758                 :             : 
     759         [ +  - ]:           7 :         Assert(obj_desc);
     760                 :             : 
     761                 :             :         /* enforce writability because snapshot is probably wrong otherwise */
     762         [ +  - ]:           7 :         if ((obj_desc->flags & IFS_WRLOCK) == 0)
     763   [ #  #  #  # ]:           0 :                 ereport(ERROR,
     764                 :             :                                 (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
     765                 :             :                                  errmsg("permission denied for large object %u",
     766                 :             :                                                 obj_desc->id)));
     767                 :             : 
     768                 :             :         /*
     769                 :             :          * use errmsg_internal here because we don't want to expose INT64_FORMAT
     770                 :             :          * in translatable strings; doing better is not worth the trouble
     771                 :             :          */
     772         [ +  - ]:           7 :         if (len < 0 || len > MAX_LARGE_OBJECT_SIZE)
     773   [ #  #  #  # ]:           0 :                 ereport(ERROR,
     774                 :             :                                 (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
     775                 :             :                                  errmsg_internal("invalid large object truncation target: " INT64_FORMAT,
     776                 :             :                                                                  len)));
     777                 :             : 
     778                 :           7 :         open_lo_relation();
     779                 :             : 
     780                 :           7 :         indstate = CatalogOpenIndexes(lo_heap_r);
     781                 :             : 
     782                 :             :         /*
     783                 :             :          * Set up to find all pages with desired loid and pageno >= target
     784                 :             :          */
     785                 :          14 :         ScanKeyInit(&skey[0],
     786                 :             :                                 Anum_pg_largeobject_loid,
     787                 :             :                                 BTEqualStrategyNumber, F_OIDEQ,
     788                 :           7 :                                 ObjectIdGetDatum(obj_desc->id));
     789                 :             : 
     790                 :          14 :         ScanKeyInit(&skey[1],
     791                 :             :                                 Anum_pg_largeobject_pageno,
     792                 :             :                                 BTGreaterEqualStrategyNumber, F_INT4GE,
     793                 :           7 :                                 Int32GetDatum(pageno));
     794                 :             : 
     795                 :          14 :         sd = systable_beginscan_ordered(lo_heap_r, lo_index_r,
     796                 :           7 :                                                                         obj_desc->snapshot, 2, skey);
     797                 :             : 
     798                 :             :         /*
     799                 :             :          * If possible, get the page the truncation point is in. The truncation
     800                 :             :          * point may be beyond the end of the LO or in a hole.
     801                 :             :          */
     802                 :           7 :         olddata = NULL;
     803         [ +  + ]:           7 :         if ((oldtuple = systable_getnext_ordered(sd, ForwardScanDirection)) != NULL)
     804                 :             :         {
     805         [ +  - ]:           4 :                 if (HeapTupleHasNulls(oldtuple))        /* paranoia */
     806   [ #  #  #  # ]:           0 :                         elog(ERROR, "null field found in pg_largeobject");
     807                 :           4 :                 olddata = (Form_pg_largeobject) GETSTRUCT(oldtuple);
     808         [ +  - ]:           4 :                 Assert(olddata->pageno >= pageno);
     809                 :           4 :         }
     810                 :             : 
     811                 :             :         /*
     812                 :             :          * If we found the page of the truncation point we need to truncate the
     813                 :             :          * data in it.  Otherwise if we're in a hole, we need to create a page to
     814                 :             :          * mark the end of data.
     815                 :             :          */
     816   [ +  +  +  + ]:           7 :         if (olddata != NULL && olddata->pageno == pageno)
     817                 :             :         {
     818                 :             :                 /* First, load old data into workbuf */
     819                 :           2 :                 bytea      *datafield;
     820                 :           2 :                 int                     pagelen;
     821                 :           2 :                 bool            pfreeit;
     822                 :             : 
     823                 :           2 :                 getdatafield(olddata, &datafield, &pagelen, &pfreeit);
     824                 :           2 :                 memcpy(workb, VARDATA(datafield), pagelen);
     825         [ +  + ]:           2 :                 if (pfreeit)
     826                 :           1 :                         pfree(datafield);
     827                 :             : 
     828                 :             :                 /*
     829                 :             :                  * Fill any hole
     830                 :             :                  */
     831                 :           2 :                 off = len % LOBLKSIZE;
     832         [ +  + ]:           2 :                 if (off > pagelen)
     833   [ -  +  #  #  :           1 :                         MemSet(workb + pagelen, 0, off - pagelen);
          #  #  #  #  #  
                      # ]
     834                 :             : 
     835                 :             :                 /* compute length of new page */
     836                 :           2 :                 SET_VARSIZE(&workbuf.hdr, off + VARHDRSZ);
     837                 :             : 
     838                 :             :                 /*
     839                 :             :                  * Form and insert updated tuple
     840                 :             :                  */
     841                 :           2 :                 memset(values, 0, sizeof(values));
     842                 :           2 :                 memset(nulls, false, sizeof(nulls));
     843                 :           2 :                 memset(replace, false, sizeof(replace));
     844                 :           2 :                 values[Anum_pg_largeobject_data - 1] = PointerGetDatum(&workbuf);
     845                 :           2 :                 replace[Anum_pg_largeobject_data - 1] = true;
     846                 :           4 :                 newtup = heap_modify_tuple(oldtuple, RelationGetDescr(lo_heap_r),
     847                 :           2 :                                                                    values, nulls, replace);
     848                 :           4 :                 CatalogTupleUpdateWithInfo(lo_heap_r, &newtup->t_self, newtup,
     849                 :           2 :                                                                    indstate);
     850                 :           2 :                 heap_freetuple(newtup);
     851                 :           2 :         }
     852                 :             :         else
     853                 :             :         {
     854                 :             :                 /*
     855                 :             :                  * If the first page we found was after the truncation point, we're in
     856                 :             :                  * a hole that we'll fill, but we need to delete the later page
     857                 :             :                  * because the loop below won't visit it again.
     858                 :             :                  */
     859         [ +  + ]:           5 :                 if (olddata != NULL)
     860                 :             :                 {
     861         [ +  - ]:           2 :                         Assert(olddata->pageno > pageno);
     862                 :           2 :                         CatalogTupleDelete(lo_heap_r, &oldtuple->t_self);
     863                 :           2 :                 }
     864                 :             : 
     865                 :             :                 /*
     866                 :             :                  * Write a brand new page.
     867                 :             :                  *
     868                 :             :                  * Fill the hole up to the truncation point
     869                 :             :                  */
     870                 :           5 :                 off = len % LOBLKSIZE;
     871         [ -  + ]:           5 :                 if (off > 0)
     872   [ +  -  +  +  :         182 :                         MemSet(workb, 0, off);
          +  -  +  +  +  
                      + ]
     873                 :             : 
     874                 :             :                 /* compute length of new page */
     875                 :           5 :                 SET_VARSIZE(&workbuf.hdr, off + VARHDRSZ);
     876                 :             : 
     877                 :             :                 /*
     878                 :             :                  * Form and insert new tuple
     879                 :             :                  */
     880                 :           5 :                 memset(values, 0, sizeof(values));
     881                 :           5 :                 memset(nulls, false, sizeof(nulls));
     882                 :           5 :                 values[Anum_pg_largeobject_loid - 1] = ObjectIdGetDatum(obj_desc->id);
     883                 :           5 :                 values[Anum_pg_largeobject_pageno - 1] = Int32GetDatum(pageno);
     884                 :           5 :                 values[Anum_pg_largeobject_data - 1] = PointerGetDatum(&workbuf);
     885                 :           5 :                 newtup = heap_form_tuple(lo_heap_r->rd_att, values, nulls);
     886                 :           5 :                 CatalogTupleInsertWithInfo(lo_heap_r, newtup, indstate);
     887                 :           5 :                 heap_freetuple(newtup);
     888                 :             :         }
     889                 :             : 
     890                 :             :         /*
     891                 :             :          * Delete any pages after the truncation point.  If the initial search
     892                 :             :          * didn't find a page, then of course there's nothing more to do.
     893                 :             :          */
     894         [ +  + ]:           7 :         if (olddata != NULL)
     895                 :             :         {
     896         [ +  + ]:           5 :                 while ((oldtuple = systable_getnext_ordered(sd, ForwardScanDirection)) != NULL)
     897                 :             :                 {
     898                 :           1 :                         CatalogTupleDelete(lo_heap_r, &oldtuple->t_self);
     899                 :             :                 }
     900                 :           4 :         }
     901                 :             : 
     902                 :           7 :         systable_endscan_ordered(sd);
     903                 :             : 
     904                 :           7 :         CatalogCloseIndexes(indstate);
     905                 :             : 
     906                 :             :         /*
     907                 :             :          * Advance command counter so that tuple updates will be seen by later
     908                 :             :          * large-object operations in this transaction.
     909                 :             :          */
     910                 :           7 :         CommandCounterIncrement();
     911                 :           7 : }
        

Generated by: LCOV version 2.3.2-1