Coverage Report

Created: 2026-06-08 06:07

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/util-linux/libblkid/src/superblocks/iso9660.c
Line
Count
Source
1
/*
2
 * Copyright (C) 1999 by Andries Brouwer
3
 * Copyright (C) 1999, 2000, 2003 by Theodore Ts'o
4
 * Copyright (C) 2001 by Andreas Dilger
5
 * Copyright (C) 2004 Kay Sievers <kay.sievers@vrfy.org>
6
 * Copyright (C) 2008 Karel Zak <kzak@redhat.com>
7
 *
8
 * Inspired also by libvolume_id by
9
 *     Kay Sievers <kay.sievers@vrfy.org>
10
 *
11
 * This file may be redistributed under the terms of the
12
 * GNU Lesser General Public License.
13
 */
14
#include <stdio.h>
15
#include <stdlib.h>
16
#include <unistd.h>
17
#include <string.h>
18
#include <stdint.h>
19
#include <ctype.h>
20
21
#include "superblocks.h"
22
#include "cctype.h"
23
#include "iso9660.h"
24
25
struct hs_date {
26
  unsigned char year[4];
27
  unsigned char month[2];
28
  unsigned char day[2];
29
  unsigned char hour[2];
30
  unsigned char minute[2];
31
  unsigned char second[2];
32
  unsigned char hundredth[2];
33
} __attribute__ ((packed));
34
35
struct iso9660_date {
36
  struct hs_date common;
37
  unsigned char offset;
38
} __attribute__ ((packed));
39
40
/* PVD - Primary volume descriptor */
41
struct iso_volume_descriptor {
42
  /* High Sierra has 8 bytes before descriptor with Volume Descriptor LBN value, those are skipped by blkid_probe_get_buffer() */
43
  unsigned char vd_type;
44
  unsigned char vd_id[5];
45
  unsigned char vd_version;
46
  unsigned char flags;
47
  unsigned char system_id[32];
48
  unsigned char volume_id[32];
49
  unsigned char unused[8];
50
  unsigned char space_size[8];
51
  unsigned char escape_sequences[32];
52
  unsigned char  set_size[4];
53
  unsigned char  vol_seq_num[4];
54
  unsigned char  logical_block_size[4];
55
  unsigned char  path_table_size[8];
56
  union {
57
    struct {
58
      unsigned char type_l_path_table[4];
59
      unsigned char opt_type_l_path_table[4];
60
      unsigned char type_m_path_table[4];
61
      unsigned char opt_type_m_path_table[4];
62
      unsigned char root_dir_record[34];
63
      unsigned char volume_set_id[128];
64
      unsigned char publisher_id[128];
65
      unsigned char data_preparer_id[128];
66
      unsigned char application_id[128];
67
      unsigned char copyright_file_id[37];
68
      unsigned char abstract_file_id[37];
69
      unsigned char bibliographic_file_id[37];
70
      struct iso9660_date created;
71
      struct iso9660_date modified;
72
      struct iso9660_date expiration;
73
      struct iso9660_date effective;
74
      unsigned char std_version;
75
    } iso; /* ISO9660 */
76
    struct {
77
      unsigned char type_l_path_table[4];
78
      unsigned char opt1_type_l_path_table[4];
79
      unsigned char opt2_type_l_path_table[4];
80
      unsigned char opt3_type_l_path_table[4];
81
      unsigned char type_m_path_table[4];
82
      unsigned char opt1_type_m_path_table[4];
83
      unsigned char opt2_type_m_path_table[4];
84
      unsigned char opt3_type_m_path_table[4];
85
      unsigned char root_dir_record[34];
86
      unsigned char volume_set_id[128];
87
      unsigned char publisher_id[128];
88
      unsigned char data_preparer_id[128];
89
      unsigned char application_id[128];
90
      unsigned char copyright_file_id[32];
91
      unsigned char abstract_file_id[32];
92
      struct hs_date created;
93
      struct hs_date modified;
94
      struct hs_date expiration;
95
      struct hs_date effective;
96
      unsigned char std_version;
97
    } hs; /* High Sierra */
98
  };
99
} __attribute__((packed));
100
101
/* Boot Record */
102
struct boot_record {
103
  /* High Sierra has 8 bytes before descriptor with Volume Descriptor LBN value, those are skipped by blkid_probe_get_buffer() */
104
  unsigned char vd_type;
105
  unsigned char vd_id[5];
106
  unsigned char vd_version;
107
  unsigned char boot_system_id[32];
108
  unsigned char boot_id[32];
109
  unsigned char unused[1];
110
} __attribute__((packed));
111
112
477
#define ISO_SUPERBLOCK_OFFSET   0x8000
113
6.18k
#define ISO_SECTOR_SIZE     0x800
114
2.36k
#define ISO_VD_BOOT_RECORD    0x0
115
4.26k
#define ISO_VD_PRIMARY      0x1
116
3.59k
#define ISO_VD_SUPPLEMENTARY    0x2
117
5.79k
#define ISO_VD_END      0xff
118
12.3k
#define ISO_VD_MAX      16
119
/* maximal string field size used anywhere in ISO; update if necessary */
120
#define ISO_MAX_FIELDSIZ  sizeof_member(struct iso_volume_descriptor, iso.volume_set_id)
121
122
static int probe_iso9660_set_uuid (blkid_probe pr, const struct hs_date *date, unsigned char offset)
123
64
{
124
64
  unsigned char buffer[16];
125
64
  unsigned int i, zeros = 0;
126
127
64
  buffer[0] = date->year[0];
128
64
  buffer[1] = date->year[1];
129
64
  buffer[2] = date->year[2];
130
64
  buffer[3] = date->year[3];
131
64
  buffer[4] = date->month[0];
132
64
  buffer[5] = date->month[1];
133
64
  buffer[6] = date->day[0];
134
64
  buffer[7] = date->day[1];
135
64
  buffer[8] = date->hour[0];
136
64
  buffer[9] = date->hour[1];
137
64
  buffer[10] = date->minute[0];
138
64
  buffer[11] = date->minute[1];
139
64
  buffer[12] = date->second[0];
140
64
  buffer[13] = date->second[1];
141
64
  buffer[14] = date->hundredth[0];
142
64
  buffer[15] = date->hundredth[1];
143
144
  /* count the number of zeros ('0') in the date buffer */
145
1.08k
  for (i = 0, zeros = 0; i < sizeof(buffer); i++)
146
1.02k
    if (buffer[i] == '0')
147
221
      zeros++;
148
149
  /* due to the iso9660 standard if all date fields are '0' and offset is 0, the date is unset */
150
64
  if (zeros == sizeof(buffer) && offset == 0)
151
7
    return 0;
152
153
  /* generate an UUID using this date and return success */
154
57
  blkid_probe_sprintf_uuid (pr, buffer, sizeof(buffer),
155
57
    "%c%c%c%c-%c%c-%c%c-%c%c-%c%c-%c%c-%c%c",
156
57
    buffer[0], buffer[1], buffer[2], buffer[3],
157
57
    buffer[4], buffer[5],
158
57
    buffer[6], buffer[7],
159
57
    buffer[8], buffer[9],
160
57
    buffer[10], buffer[11],
161
57
    buffer[12], buffer[13],
162
57
    buffer[14], buffer[15]);
163
164
57
  return 1;
165
64
}
166
167
static int is_str_empty(const unsigned char *str, size_t len)
168
177
{
169
177
  size_t i;
170
171
177
  if (!str || !*str)
172
50
    return 1;
173
174
1.84k
  for (i = 0; i < len; i++)
175
1.83k
    if (!isspace(str[i]))
176
121
      return 0;
177
6
  return 1;
178
127
}
179
180
static int is_utf16be_str_empty(unsigned char *utf16, size_t len)
181
54
{
182
54
  size_t i;
183
184
54
  for (i = 0; i < len; i += 2) {
185
54
    if (utf16[i] != 0x0 || !isspace(utf16[i + 1]))
186
54
      return 0;
187
54
  }
188
0
  return 1;
189
54
}
190
191
/* if @utf16 is prefix of @ascii (ignoring non-representable characters and upper-case conversion)
192
 * then reconstruct prefix from @utf16 and @ascii, append suffix from @ascii, fill it into @out
193
 * and returns length of bytes written into @out; otherwise returns zero */
194
static size_t merge_utf16be_ascii(unsigned char *out, size_t out_len, const unsigned char *utf16, const unsigned char *ascii, size_t len)
195
89
{
196
89
  size_t o, a, u;
197
198
111
  for (o = 0, a = 0, u = 0; u + 1 < len && a < len && o + 1 < out_len; o += 2, a++, u += 2) {
199
    /* Surrogate pair with code point above U+FFFF */
200
111
    if (utf16[u] >= 0xD8 && utf16[u] <= 0xDB && u + 3 < len &&
201
1
        utf16[u + 2] >= 0xDC && utf16[u + 2] <= 0xDF) {
202
0
      out[o++] = utf16[u++];
203
0
      out[o++] = utf16[u++];
204
0
    }
205
    /* Value '_' is replacement for non-representable character */
206
111
    if (ascii[a] == '_') {
207
0
      out[o] = utf16[u];
208
0
      out[o + 1] = utf16[u + 1];
209
111
    } else if (utf16[u] == 0x00 && utf16[u + 1] == '_') {
210
0
      out[o] = 0x00;
211
0
      out[o + 1] = ascii[a];
212
111
    } else if (utf16[u] == 0x00 && c_toupper(ascii[a]) == c_toupper(utf16[u + 1])) {
213
22
      out[o] = 0x00;
214
22
      out[o + 1] = c_isupper(ascii[a]) ? utf16[u + 1] : ascii[a];
215
89
    } else {
216
89
      return 0;
217
89
    }
218
111
  }
219
220
0
  for (; a < len && o + 1 < out_len; o += 2, a++) {
221
0
    out[o] = 0x00;
222
0
    out[o + 1] = ascii[a];
223
0
  }
224
225
0
  return o;
226
89
}
227
228
/* iso9660 [+ Microsoft Joliet Extension] */
229
static int probe_iso9660(blkid_probe pr, const struct blkid_idmag *mag)
230
477
{
231
477
  struct boot_record *boot = NULL;
232
477
  struct iso_volume_descriptor *pvd = NULL;
233
477
  struct iso_volume_descriptor *joliet = NULL;
234
  /* space for merge_utf16be_ascii(ISO_ID_BUFSIZ bytes) */
235
477
  unsigned char buf[ISO_MAX_FIELDSIZ * 5 / 2];
236
477
  const struct hs_date *modified;
237
477
  const struct hs_date *created;
238
477
  unsigned char modified_offset;
239
477
  unsigned char created_offset;
240
477
  size_t len;
241
477
  int is_hs;
242
477
  int is_unicode_empty;
243
477
  int is_ascii_hs_empty;
244
477
  int is_ascii_iso_empty;
245
477
  int i;
246
477
  uint64_t off;
247
248
477
  if (blkid_probe_get_hint(pr, mag->hoff, &off) < 0)
249
477
    off = 0;
250
251
477
  if (off % ISO_SECTOR_SIZE)
252
0
    return 1;
253
254
477
  is_hs = (strcmp(mag->magic, "CDROM") == 0);
255
256
6.18k
  for (i = 0, off += ISO_SUPERBLOCK_OFFSET; i < ISO_VD_MAX && (!boot || !pvd || (!is_hs && !joliet)); i++, off += ISO_SECTOR_SIZE) {
257
5.79k
    const unsigned char *desc =
258
5.79k
      blkid_probe_get_buffer(pr,
259
5.79k
          off + (is_hs ? 8 : 0), /* High Sierra has 8 bytes before descriptor with Volume Descriptor LBN value */
260
5.79k
          max(sizeof(struct boot_record),
261
5.79k
              sizeof(struct iso_volume_descriptor)));
262
263
5.79k
    if (desc == NULL || desc[0] == ISO_VD_END)
264
84
      break;
265
5.71k
    else if (!boot && desc[0] == ISO_VD_BOOT_RECORD)
266
437
      boot = (struct boot_record *)desc;
267
5.27k
    else if (!pvd && desc[0] == ISO_VD_PRIMARY)
268
311
      pvd = (struct iso_volume_descriptor *)desc;
269
4.96k
    else if (!is_hs && !joliet && desc[0] == ISO_VD_SUPPLEMENTARY) {
270
474
      joliet = (struct iso_volume_descriptor *)desc;
271
474
      if (memcmp(joliet->escape_sequences, "%/@", 3) != 0 &&
272
464
          memcmp(joliet->escape_sequences, "%/C", 3) != 0 &&
273
459
          memcmp(joliet->escape_sequences, "%/E", 3) != 0)
274
429
        joliet = NULL;
275
474
    }
276
5.79k
  }
277
278
477
  if (!pvd)
279
166
    return errno ? -errno : 1;
280
281
311
  uint16_t logical_block_size = isonum_723(pvd->logical_block_size, false);
282
311
  uint32_t space_size = isonum_733(pvd->space_size, false);
283
284
311
  if (logical_block_size == 0)
285
71
    return 1;
286
287
  /*
288
   * Verify the root directory record to reduce false positives.
289
   *
290
   * The CD001 magic at 32KB can match data content on other
291
   * filesystems (e.g. an .iso file stored on XFS). Validate that
292
   * the root directory extent actually contains a valid directory
293
   * entry -- the first entry must be the "." self-reference with
294
   * file_id = 0x00 pointing back to the same LBA.
295
   */
296
240
  {
297
240
    const unsigned char *rdr = is_hs ?
298
128
      pvd->hs.root_dir_record : pvd->iso.root_dir_record;
299
240
    uint32_t root_lba = isonum_731(rdr + 2);
300
240
    uint32_t root_len = isonum_731(rdr + 10);
301
240
    uint64_t root_off = (uint64_t) root_lba * logical_block_size;
302
240
    const unsigned char *rootdata;
303
304
240
    if (root_len == 0)
305
19
      return 1;
306
307
221
    rootdata = blkid_probe_get_buffer(pr, root_off,
308
221
        root_len < 2048 ? root_len : 2048);
309
221
    if (!rootdata)
310
66
      return errno ? -errno : 1;
311
312
    /* The first directory entry must be "." (self-reference):
313
     *   - dr_len >= 34 (minimum directory record size)
314
     *   - file_id_len == 1
315
     *   - file_id == 0x00 (the "." identifier)
316
     *   - extent location == root_lba (points to itself)
317
     */
318
155
    if (rootdata[0] < 34 ||
319
136
        rootdata[32] != 1 ||
320
121
        rootdata[33] != 0x00 ||
321
118
        isonum_731(rootdata + 2) != root_lba) {
322
96
      DBG(LOWPROBE, ul_debug("ISO9660: root dir validation "
323
96
        "failed (false positive CD001 signature?)"));
324
96
      return 1;
325
96
    }
326
155
  }
327
328
59
  blkid_probe_set_fsblocksize(pr, logical_block_size);
329
59
  blkid_probe_set_block_size(pr, logical_block_size);
330
59
  blkid_probe_set_fssize(pr, (uint64_t) space_size * logical_block_size);
331
332
59
  if (joliet && (len = merge_utf16be_ascii(buf, sizeof(buf), joliet->system_id, pvd->system_id, sizeof(pvd->system_id))) != 0)
333
0
    blkid_probe_set_utf8_id_label(pr, "SYSTEM_ID", buf, len, UL_ENCODE_UTF16BE);
334
59
  else if (joliet)
335
18
    blkid_probe_set_utf8_id_label(pr, "SYSTEM_ID", joliet->system_id, sizeof(joliet->system_id), UL_ENCODE_UTF16BE);
336
41
  else
337
41
    blkid_probe_set_id_label(pr, "SYSTEM_ID", pvd->system_id, sizeof(pvd->system_id));
338
339
59
  if (joliet && (len = merge_utf16be_ascii(buf, sizeof(buf), joliet->iso.volume_set_id, pvd->iso.volume_set_id, sizeof(pvd->iso.volume_set_id))) != 0)
340
0
    blkid_probe_set_utf8_id_label(pr, "VOLUME_SET_ID", buf, len, UL_ENCODE_UTF16BE);
341
59
  else if (joliet)
342
18
    blkid_probe_set_utf8_id_label(pr, "VOLUME_SET_ID", joliet->iso.volume_set_id, sizeof(joliet->iso.volume_set_id), UL_ENCODE_UTF16BE);
343
41
  else if (is_hs)
344
31
    blkid_probe_set_id_label(pr, "VOLUME_SET_ID", pvd->hs.volume_set_id, sizeof(pvd->hs.volume_set_id));
345
10
  else
346
10
    blkid_probe_set_id_label(pr, "VOLUME_SET_ID", pvd->iso.volume_set_id, sizeof(pvd->iso.volume_set_id));
347
348
59
  is_ascii_hs_empty = (!is_hs || is_str_empty(pvd->hs.publisher_id, sizeof(pvd->hs.publisher_id)) || pvd->hs.publisher_id[0] == '_');
349
59
  is_ascii_iso_empty = (is_hs || is_str_empty(pvd->iso.publisher_id, sizeof(pvd->iso.publisher_id)) || pvd->iso.publisher_id[0] == '_');
350
59
  is_unicode_empty = (!joliet || is_utf16be_str_empty(joliet->iso.publisher_id, sizeof(joliet->iso.publisher_id)) || (joliet->iso.publisher_id[0] == 0x00 && joliet->iso.publisher_id[1] == '_'));
351
59
  if (!is_unicode_empty && !is_ascii_iso_empty && (len = merge_utf16be_ascii(buf, sizeof(buf), joliet->iso.publisher_id, pvd->iso.publisher_id, sizeof(pvd->iso.publisher_id))) != 0)
352
0
    blkid_probe_set_utf8_id_label(pr, "PUBLISHER_ID", buf, len, UL_ENCODE_UTF16BE);
353
59
  else if (!is_unicode_empty)
354
18
    blkid_probe_set_utf8_id_label(pr, "PUBLISHER_ID", joliet->iso.publisher_id, sizeof(joliet->iso.publisher_id), UL_ENCODE_UTF16BE);
355
41
  else if (!is_ascii_hs_empty)
356
17
    blkid_probe_set_id_label(pr, "PUBLISHER_ID", pvd->hs.publisher_id, sizeof(pvd->hs.publisher_id));
357
24
  else if (!is_ascii_iso_empty)
358
9
    blkid_probe_set_id_label(pr, "PUBLISHER_ID", pvd->iso.publisher_id, sizeof(pvd->iso.publisher_id));
359
360
59
  is_ascii_hs_empty = (!is_hs || is_str_empty(pvd->hs.data_preparer_id, sizeof(pvd->hs.data_preparer_id)) || pvd->hs.data_preparer_id[0] == '_');
361
59
  is_ascii_iso_empty = (is_hs || is_str_empty(pvd->iso.data_preparer_id, sizeof(pvd->iso.data_preparer_id)) || pvd->iso.data_preparer_id[0] == '_');
362
59
  is_unicode_empty = (!joliet || is_utf16be_str_empty(joliet->iso.data_preparer_id, sizeof(joliet->iso.data_preparer_id)) || (joliet->iso.data_preparer_id[0] == 0x00 && joliet->iso.data_preparer_id[1] == '_'));
363
59
  if (!is_unicode_empty && !is_ascii_iso_empty && (len = merge_utf16be_ascii(buf, sizeof(buf), joliet->iso.data_preparer_id, pvd->iso.data_preparer_id, sizeof(pvd->iso.data_preparer_id))) != 0)
364
0
    blkid_probe_set_utf8_id_label(pr, "DATA_PREPARER_ID", buf, len, UL_ENCODE_UTF16BE);
365
59
  else if (!is_unicode_empty)
366
18
    blkid_probe_set_utf8_id_label(pr, "DATA_PREPARER_ID", joliet->iso.data_preparer_id, sizeof(joliet->iso.data_preparer_id), UL_ENCODE_UTF16BE);
367
41
  else if (!is_ascii_hs_empty)
368
21
    blkid_probe_set_id_label(pr, "DATA_PREPARER_ID", pvd->hs.data_preparer_id, sizeof(pvd->hs.data_preparer_id));
369
20
  else if (!is_ascii_iso_empty)
370
9
    blkid_probe_set_id_label(pr, "DATA_PREPARER_ID", pvd->iso.data_preparer_id, sizeof(pvd->iso.data_preparer_id));
371
372
59
  is_ascii_hs_empty = (!is_hs || is_str_empty(pvd->hs.application_id, sizeof(pvd->hs.application_id)) || pvd->hs.application_id[0] == '_');
373
59
  is_ascii_iso_empty = (is_hs || is_str_empty(pvd->iso.application_id, sizeof(pvd->iso.application_id)) || pvd->iso.application_id[0] == '_');
374
59
  is_unicode_empty = (!joliet || is_utf16be_str_empty(joliet->iso.application_id, sizeof(joliet->iso.application_id)) || (joliet->iso.application_id[0] == 0x00 && joliet->iso.application_id[1] == '_'));
375
59
  if (!is_unicode_empty && !is_ascii_iso_empty && (len = merge_utf16be_ascii(buf, sizeof(buf), joliet->iso.application_id, pvd->iso.application_id, sizeof(pvd->iso.application_id))) != 0)
376
0
    blkid_probe_set_utf8_id_label(pr, "APPLICATION_ID", buf, len, UL_ENCODE_UTF16BE);
377
59
  else if (!is_unicode_empty)
378
18
    blkid_probe_set_utf8_id_label(pr, "APPLICATION_ID", joliet->iso.application_id, sizeof(joliet->iso.application_id), UL_ENCODE_UTF16BE);
379
41
  else if (!is_ascii_hs_empty)
380
23
    blkid_probe_set_id_label(pr, "APPLICATION_ID", pvd->hs.application_id, sizeof(pvd->hs.application_id));
381
18
  else if (!is_ascii_iso_empty)
382
7
    blkid_probe_set_id_label(pr, "APPLICATION_ID", pvd->iso.application_id, sizeof(pvd->iso.application_id));
383
384
59
  if (is_hs) {
385
31
    modified = &pvd->hs.modified;
386
31
    created = &pvd->hs.created;
387
31
    modified_offset = 0;
388
31
    created_offset = 0;
389
31
  } else {
390
28
    modified = &pvd->iso.modified.common;
391
28
    created = &pvd->iso.created.common;
392
28
    modified_offset = pvd->iso.modified.offset;
393
28
    created_offset = pvd->iso.created.offset;
394
28
  }
395
396
  /* create an UUID using the modified/created date */
397
59
  if (! probe_iso9660_set_uuid(pr, modified, modified_offset))
398
5
    probe_iso9660_set_uuid(pr, created, created_offset);
399
400
59
  if (boot)
401
57
    blkid_probe_set_id_label(pr, "BOOT_SYSTEM_ID",
402
57
          boot->boot_system_id,
403
57
          sizeof(boot->boot_system_id));
404
405
59
  if (joliet)
406
18
    blkid_probe_set_version(pr, "Joliet Extension");
407
41
  else if (is_hs)
408
31
    blkid_probe_set_version(pr, "High Sierra");
409
410
  /* Label in Joliet is UNICODE (UTF16BE) but can contain only 16 characters. Label in PVD is
411
   * subset of ASCII but can contain up to the 32 characters. Non-representable characters are
412
   * stored as replacement character '_'. Label in Joliet is in most cases trimmed but UNICODE
413
   * version of label in PVD. Based on these facts try to reconstruct original label if label
414
   * in Joliet is prefix of the label in PVD (ignoring non-representable characters).
415
   */
416
59
  if (joliet && (len = merge_utf16be_ascii(buf, sizeof(buf), joliet->volume_id, pvd->volume_id, sizeof(pvd->volume_id))) != 0)
417
0
    blkid_probe_set_utf8label(pr, buf, len, UL_ENCODE_UTF16BE);
418
59
  else if (joliet)
419
18
    blkid_probe_set_utf8label(pr, joliet->volume_id, sizeof(joliet->volume_id), UL_ENCODE_UTF16BE);
420
41
  else
421
41
    blkid_probe_set_label(pr, pvd->volume_id, sizeof(pvd->volume_id));
422
423
59
  return 0;
424
155
}
425
426
const struct blkid_idinfo iso9660_idinfo =
427
{
428
  .name   = "iso9660",
429
  .usage    = BLKID_USAGE_FILESYSTEM,
430
  .probefunc  = probe_iso9660,
431
  .flags    = BLKID_IDINFO_TOLERANT,
432
  .magics   =
433
  {
434
    /*
435
     * Due to different location of vd_id[] in ISO9660 and High Sierra, IS9660 can match
436
     * also High Sierra vd_id[]. So always check ISO9660 (CD001) before High Sierra (CDROM).
437
     */
438
    { .magic = "CD001", .len = 5, .kboff = 32, .sboff = 1, .hoff = "session_offset" },
439
    { .magic = "CDROM", .len = 5, .kboff = 32, .sboff = 9, .hoff = "session_offset" },
440
    { NULL }
441
  }
442
};
443