Coverage Report

Created: 2026-09-14 06:37

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libetonyek/src/lib/EtonyekDocument.cpp
Line
Count
Source
1
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2
/*
3
 * This file is part of the libetonyek project.
4
 *
5
 * This Source Code Form is subject to the terms of the Mozilla Public
6
 * License, v. 2.0. If a copy of the MPL was not distributed with this
7
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8
 */
9
10
#include <libetonyek/libetonyek.h>
11
12
#include <cassert>
13
#include <cstring>
14
#include <memory>
15
16
#include <boost/algorithm/string/predicate.hpp>
17
#include <boost/optional.hpp>
18
19
#include <libxml/xmlreader.h>
20
21
#include "libetonyek_utils.h"
22
#include "libetonyek_xml.h"
23
#include "IWAMessage.h"
24
#include "IWASnappyStream.h"
25
#include "IWORKPresentationRedirector.h"
26
#include "IWORKSpreadsheetRedirector.h"
27
#include "IWORKSubDirStream.h"
28
#include "IWORKTextRedirector.h"
29
#include "IWORKTokenizer.h"
30
#include "IWORKZlibStream.h"
31
#include "KEY1Dictionary.h"
32
#include "KEY1Parser.h"
33
#include "KEY1Token.h"
34
#include "KEY2Dictionary.h"
35
#include "KEY2Parser.h"
36
#include "KEY2Token.h"
37
#include "KEY6Parser.h"
38
#include "KEYCollector.h"
39
#include "NUMCollector.h"
40
#include "NUM1Dictionary.h"
41
#include "NUM1Parser.h"
42
#include "NUM1Token.h"
43
#include "NUM3Parser.h"
44
#include "PAGCollector.h"
45
#include "PAG1Dictionary.h"
46
#include "PAG1Parser.h"
47
#include "PAG1Token.h"
48
#include "PAG5Parser.h"
49
50
51
using std::shared_ptr;
52
using std::string;
53
54
using librevenge::RVNG_SEEK_SET;
55
56
namespace libetonyek
57
{
58
59
namespace
60
{
61
62
enum Format
63
{
64
  FORMAT_UNKNOWN,
65
  FORMAT_XML1,
66
  FORMAT_XML2,
67
  FORMAT_BINARY
68
};
69
70
struct DetectionInfo
71
{
72
  explicit DetectionInfo(EtonyekDocument::Type type = EtonyekDocument::TYPE_UNKNOWN);
73
74
  RVNGInputStreamPtr_t m_input;
75
  RVNGInputStreamPtr_t m_package;
76
  RVNGInputStreamPtr_t m_fragments;
77
  EtonyekDocument::Confidence m_confidence;
78
  EtonyekDocument::Type m_type;
79
  Format m_format;
80
};
81
82
DetectionInfo::DetectionInfo(const EtonyekDocument::Type type)
83
24.9k
  : m_input()
84
24.9k
  , m_package()
85
24.9k
  , m_fragments()
86
24.9k
  , m_confidence(EtonyekDocument::CONFIDENCE_NONE)
87
24.9k
  , m_type(type)
88
24.9k
  , m_format(FORMAT_UNKNOWN)
89
24.9k
{
90
24.9k
}
91
92
bool probeXMLFormat(const Format format, const EtonyekDocument::Type type, const int docId,
93
                    const IWORKTokenizer &tokenizer, const char *const name, const char *const ns,
94
                    DetectionInfo &info)
95
3.57k
{
96
3.57k
  if (((info.m_format == format) || (info.m_format == FORMAT_UNKNOWN))
97
3.57k
      && ((info.m_type == type) || info.m_type == EtonyekDocument::TYPE_UNKNOWN))
98
1.78k
  {
99
1.78k
    if (tokenizer.getQualifiedId(name, ns) == docId)
100
0
    {
101
0
      info.m_format = format;
102
0
      info.m_type = type;
103
0
      return true;
104
0
    }
105
1.78k
  }
106
3.57k
  return false;
107
3.57k
}
108
109
namespace
110
{
111
void handleError(void * /*arg*/, const char * /*msg*/, xmlParserSeverities /*severity*/, xmlTextReaderLocatorPtr /*locator*/)
112
474k
{
113
474k
}
114
}
115
116
bool probeXML(DetectionInfo &info)
117
20.8k
{
118
20.8k
  const auto reader = xmlReaderForStream(info.m_input);
119
20.8k
  if (!reader)
120
0
    return false;
121
122
20.8k
  xmlTextReaderSetErrorHandler(reader.get(), handleError, nullptr);
123
124
20.8k
  int ret = 0;
125
20.8k
  bool checkAPXL=false;
126
20.8k
  do
127
107k
  {
128
107k
    ret = xmlTextReaderRead(reader.get());
129
107k
    if (ret==1 && XML_READER_TYPE_DOCUMENT_TYPE==xmlTextReaderNodeType(reader.get()))
130
103
    {
131
103
      auto name = xmlTextReaderConstName(reader.get());
132
103
      if (name) checkAPXL=std::string((char const *)name)=="APXL";
133
103
    }
134
107k
  }
135
107k
  while ((1 == ret) && (XML_READER_TYPE_ELEMENT != xmlTextReaderNodeType(reader.get())));
136
137
20.8k
  if (1 != ret)
138
19.9k
    return false;
139
140
893
  const char *const name = char_cast(xmlTextReaderConstLocalName(reader.get()));
141
893
  const char *const ns = char_cast(xmlTextReaderConstNamespaceUri(reader.get()));
142
893
  if (probeXMLFormat(FORMAT_XML2, EtonyekDocument::TYPE_KEYNOTE, +KEY2Token::NS_URI_KEY | KEY2Token::presentation,
143
893
                     KEY2Token::getTokenizer(), name, ns, info))
144
0
    return true;
145
893
  if (probeXMLFormat(FORMAT_XML2, EtonyekDocument::TYPE_NUMBERS, +NUM1Token::NS_URI_LS | NUM1Token::document,
146
893
                     NUM1Token::getTokenizer(), name, ns, info))
147
0
    return true;
148
893
  if (probeXMLFormat(FORMAT_XML2, EtonyekDocument::TYPE_PAGES, +PAG1Token::NS_URI_SL | PAG1Token::document,
149
893
                     PAG1Token::getTokenizer(), name, ns, info))
150
0
    return true;
151
  // Keynote 1 files define the document type with <!DOCTYPE APXL>
152
893
  if (probeXMLFormat(FORMAT_XML1, EtonyekDocument::TYPE_KEYNOTE, +KEY1Token::NS_URI_KEY | KEY1Token::presentation,
153
893
                     KEY1Token::getTokenizer(), name, (ns||!checkAPXL) ? ns : "http://developer.apple.com/schemas/APXL", info))
154
0
    return true;
155
893
  return false;
156
893
}
157
158
bool probeBinary(DetectionInfo &info)
159
2.95k
{
160
2.95k
  const uint64_t headerLen = readUVar(info.m_input);
161
2.95k
  if (headerLen < 8)
162
4
    return false;
163
164
2.95k
  EtonyekDocument::Type detected = EtonyekDocument::TYPE_UNKNOWN;
165
166
2.95k
  const auto pos = uint64_t(info.m_input->tell());
167
2.95k
  const IWAMessage header(info.m_input, (unsigned long) headerLen);
168
169
2.95k
  if (header.uint32(1) && header.message(2) && header.message(2).uint32(1) && (header.uint32(1).get() == 1))
170
2.51k
  {
171
2.51k
    switch (header.message(2).uint32(1).get())
172
2.51k
    {
173
2.51k
    case 1 :
174
2.51k
      if (header.message(2).uint32(3))
175
2.51k
      {
176
2.51k
        uint32_t dataLen = 0;
177
2.51k
        for (auto const &infoT : header.message(2))
178
2.56k
        {
179
2.56k
          if (infoT.uint32(3)) dataLen += infoT.uint32(3).get();
180
2.56k
        }
181
2.51k
        const IWAMessage data(info.m_input, long(pos + headerLen), long(pos + headerLen + dataLen));
182
        // keynote: presentation ref in 2
183
        // number: sheet ref in 1
184
2.51k
        if (!data.message(1))
185
2.42k
          detected = EtonyekDocument::TYPE_KEYNOTE;
186
87
        else if (!data.message(2))
187
2
          detected = EtonyekDocument::TYPE_NUMBERS;
188
85
        else
189
85
        {
190
85
          unsigned potentialRef[2];
191
227
          for (unsigned test=1; test<=2; ++test)
192
154
          {
193
154
            auto ref=data.message(test).uint32(1).optional();
194
154
            if (!ref)
195
12
            {
196
12
              detected = test==1 ? EtonyekDocument::TYPE_KEYNOTE : EtonyekDocument::TYPE_NUMBERS;
197
12
              break;
198
12
            }
199
142
            potentialRef[test-1]=get(ref);
200
142
          }
201
85
          if (detected != EtonyekDocument::TYPE_UNKNOWN)
202
12
            break;
203
          // undecise, try to find the first ref
204
73
          IWAObjectIndex objIndex(info.m_fragments, info.m_package);
205
73
          objIndex.parse();
206
73
          auto type = objIndex.getObjectType(potentialRef[0]);
207
73
          detected = type && get(type)==2 ?
208
73
                     EtonyekDocument::TYPE_NUMBERS : EtonyekDocument::TYPE_KEYNOTE;
209
73
        }
210
2.51k
      }
211
2.50k
      break;
212
2.50k
    case 10000 :
213
0
      detected = EtonyekDocument::TYPE_PAGES;
214
0
      break;
215
5
    default:
216
5
      break;
217
2.51k
    }
218
2.51k
  }
219
220
2.93k
  if ((info.m_type == EtonyekDocument::TYPE_UNKNOWN) || (info.m_type == detected))
221
2.49k
  {
222
2.49k
    info.m_type = detected;
223
2.49k
    return true;
224
2.49k
  }
225
446
  return false;
226
2.93k
}
227
228
RVNGInputStreamPtr_t getSubStream(const RVNGInputStreamPtr_t &input, const char *const name)
229
99
{
230
99
  return RVNGInputStreamPtr_t(input->getSubStreamByName(name));
231
99
}
232
233
3.23k
RVNGInputStreamPtr_t getUncompressedSubStream(const RVNGInputStreamPtr_t &input, const char *const name, bool snappy = false) try
234
3.23k
{
235
3.23k
  const RVNGInputStreamPtr_t compressed(input->getSubStreamByName(name));
236
3.23k
  if (bool(compressed))
237
3.18k
  {
238
3.18k
    if (snappy)
239
3.18k
      return RVNGInputStreamPtr_t(new IWASnappyStream(compressed));
240
1
    return RVNGInputStreamPtr_t(new IWORKZlibStream(compressed));
241
3.18k
  }
242
48
  return RVNGInputStreamPtr_t();
243
3.23k
}
244
3.23k
catch (...)
245
3.23k
{
246
229
  return RVNGInputStreamPtr_t();
247
229
}
248
249
bool detectBinary(RVNGInputStreamPtr_t input, DetectionInfo &info)
250
4.49k
{
251
4.49k
  assert(input->isStructured());
252
253
4.49k
  if (input->existsSubStream("Metadata/DocumentIdentifier"))
254
671
    info.m_package = input;
255
256
4.49k
  if (input->existsSubStream("Index.zip"))
257
96
  {
258
96
    RVNGInputStreamPtr_t zipInput = getSubStream(input, "Index.zip");
259
96
    if (bool(zipInput))
260
11
      input = zipInput;
261
96
  }
262
263
4.49k
  const bool hasDocument = input->existsSubStream("Index/Document.iwa");
264
265
4.49k
  if (hasDocument)
266
3.23k
  {
267
3.23k
    info.m_format = FORMAT_BINARY;
268
3.23k
    info.m_fragments = input;
269
3.23k
    info.m_input = getUncompressedSubStream(input, "Index/Document.iwa", true);
270
3.23k
  }
271
272
4.49k
  return hasDocument;
273
4.49k
}
274
275
RVNGInputStreamPtr_t queryTopDirStream(const RVNGInputStreamPtr_t &input)
276
916
{
277
916
  assert(input->isStructured());
278
279
916
  string top;
280
281
  // find the common top level dir of all substream names, if there is one
282
3.34k
  for (unsigned i = 0; i < input->subStreamCount(); ++i)
283
2.58k
  {
284
2.58k
    const char *const path = input->subStreamName(i);
285
2.58k
    if (path)
286
2.58k
    {
287
2.58k
      if (top.empty())
288
882
      {
289
        // initialize top dir
290
882
        const char *const pos = std::strchr(path, '/');
291
882
        if (pos)
292
588
          top.assign(path, std::size_t(pos - path));
293
294
        else
294
294
          top = path;
295
882
      }
296
1.70k
      else
297
1.70k
      {
298
        // check that the current path starts with top dir
299
1.70k
        if (!boost::starts_with(path, top))
300
152
          return RVNGInputStreamPtr_t();
301
1.55k
        const char end = path[top.size()];
302
1.55k
        if (end != '/' && end != '\0')
303
2
          return RVNGInputStreamPtr_t();
304
1.55k
      }
305
2.58k
    }
306
2.58k
  }
307
308
762
  RVNGInputStreamPtr_t stream;
309
762
  if (!top.empty())
310
342
    stream.reset(new IWORKSubDirStream(input, top));
311
312
762
  return stream;
313
916
}
314
315
bool detect(const RVNGInputStreamPtr_t &input, DetectionInfo &info)
316
24.9k
{
317
24.9k
  if (input->isStructured())
318
4.14k
  {
319
4.14k
    if ((info.m_format == FORMAT_BINARY) || (info.m_format == FORMAT_UNKNOWN))
320
4.14k
    {
321
4.14k
      if (!detectBinary(input, info))
322
916
      {
323
916
        RVNGInputStreamPtr_t dir = queryTopDirStream(input);
324
916
        if (dir)
325
342
          detectBinary(dir, info);
326
916
      }
327
4.14k
    }
328
329
4.14k
    if ((info.m_format == FORMAT_XML2) || (info.m_format == FORMAT_UNKNOWN))
330
915
    {
331
915
      info.m_package = input;
332
333
915
      if ((info.m_type == EtonyekDocument::TYPE_KEYNOTE) || (info.m_type == EtonyekDocument::TYPE_UNKNOWN))
334
915
      {
335
915
        if (input->existsSubStream("index.apxl"))
336
1
        {
337
1
          info.m_format = FORMAT_XML2;
338
1
          info.m_type = EtonyekDocument::TYPE_KEYNOTE;
339
1
          info.m_input = getSubStream(input, "index.apxl");
340
1
        }
341
914
        else if (input->existsSubStream("index.apxl.gz"))
342
0
        {
343
0
          info.m_format = FORMAT_XML2;
344
0
          info.m_type = EtonyekDocument::TYPE_KEYNOTE;
345
0
          info.m_input = getUncompressedSubStream(input, "index.apxl.gz");
346
0
        }
347
915
      }
348
349
915
      if ((info.m_type == EtonyekDocument::TYPE_NUMBERS) || (info.m_type == EtonyekDocument::TYPE_PAGES) || (info.m_type == EtonyekDocument::TYPE_UNKNOWN))
350
0
      {
351
0
        if (input->existsSubStream("index.xml"))
352
0
        {
353
0
          info.m_format = FORMAT_XML2;
354
0
          info.m_input = getSubStream(input, "index.xml");
355
0
        }
356
0
        else if (input->existsSubStream("index.xml.gz"))
357
0
        {
358
0
          info.m_format = FORMAT_XML2;
359
0
          info.m_input = getUncompressedSubStream(input, "index.xml.gz");
360
0
        }
361
0
      }
362
915
    }
363
364
4.14k
    if (info.m_format == FORMAT_XML1 || info.m_format == FORMAT_UNKNOWN)
365
914
    {
366
914
      info.m_package = input;
367
368
914
      if (input->existsSubStream("presentation.apxl"))
369
2
      {
370
2
        info.m_type = EtonyekDocument::TYPE_KEYNOTE;
371
2
        info.m_format = FORMAT_XML1;
372
2
        info.m_input = getSubStream(input, "presentation.apxl");
373
2
      }
374
912
      else if (input->existsSubStream("presentation.apxl.gz"))
375
1
      {
376
1
        info.m_type = EtonyekDocument::TYPE_KEYNOTE;
377
1
        info.m_format = FORMAT_XML1;
378
1
        info.m_input = getUncompressedSubStream(input, "presentation.apxl.gz");
379
1
      }
380
914
    }
381
4.14k
  }
382
20.8k
  else
383
20.8k
  {
384
20.8k
    try
385
20.8k
    {
386
20.8k
      info.m_input = std::make_shared<IWORKZlibStream>(input);
387
20.8k
    }
388
20.8k
    catch (...)
389
20.8k
    {
390
18.2k
      info.m_input = input;
391
18.2k
    }
392
20.8k
  }
393
394
24.9k
  if (bool(info.m_input))
395
23.7k
  {
396
23.7k
    assert(!info.m_input->isStructured());
397
23.7k
    info.m_input->seek(0, RVNG_SEEK_SET);
398
399
23.7k
    bool supported = false;
400
23.7k
    if (info.m_format == FORMAT_BINARY)
401
2.95k
      supported = probeBinary(info);
402
20.8k
    else
403
20.8k
      supported = probeXML(info);
404
23.7k
    if (supported)
405
2.49k
      info.m_confidence = bool(info.m_package) ? EtonyekDocument::CONFIDENCE_EXCELLENT : EtonyekDocument::CONFIDENCE_SUPPORTED_PART;
406
23.7k
  }
407
408
24.9k
  if (info.m_confidence != EtonyekDocument::CONFIDENCE_NONE)
409
2.49k
  {
410
2.49k
    assert(EtonyekDocument::TYPE_UNKNOWN != info.m_type);
411
2.49k
    assert(FORMAT_UNKNOWN != info.m_format);
412
2.49k
    assert(bool(info.m_input));
413
2.49k
    if (info.m_confidence == EtonyekDocument::CONFIDENCE_EXCELLENT)
414
669
    {
415
669
      assert(bool(info.m_package));
416
669
    }
417
2.49k
  }
418
419
24.9k
  return info.m_confidence != EtonyekDocument::CONFIDENCE_NONE;
420
24.9k
}
421
422
}
423
424
0
ETONYEKAPI EtonyekDocument::Confidence EtonyekDocument::isSupported(librevenge::RVNGInputStream *const input, EtonyekDocument::Type *type) try
425
0
{
426
0
  if (!input)
427
0
    return CONFIDENCE_NONE;
428
429
0
  if (type)
430
0
    *type = TYPE_UNKNOWN;
431
432
0
  DetectionInfo info;
433
434
0
  if (detect(RVNGInputStreamPtr_t(input, EtonyekDummyDeleter()), info))
435
0
  {
436
0
    if (type)
437
0
      *type = info.m_type;
438
0
    return info.m_confidence;
439
0
  }
440
441
0
  return CONFIDENCE_NONE;
442
0
}
443
0
catch (...)
444
0
{
445
0
  return CONFIDENCE_NONE;
446
0
}
447
448
24.9k
ETONYEKAPI bool EtonyekDocument::parse(librevenge::RVNGInputStream *const input, librevenge::RVNGPresentationInterface *const generator) try
449
24.9k
{
450
24.9k
  if (!input || !generator)
451
0
    return false;
452
453
24.9k
  DetectionInfo info(EtonyekDocument::TYPE_KEYNOTE);
454
455
24.9k
  if (!detect(RVNGInputStreamPtr_t(input, EtonyekDummyDeleter()), info))
456
22.4k
    return false;
457
458
2.56k
  info.m_input->seek(0, librevenge::RVNG_SEEK_SET);
459
460
2.56k
  IWORKPresentationRedirector redirector(generator);
461
2.56k
  KEYCollector collector(&redirector);
462
2.56k
  if (info.m_format == FORMAT_XML1)
463
0
  {
464
0
    KEY1Dictionary dict;
465
0
    input->seek(0, librevenge::RVNG_SEEK_SET);
466
0
    shared_ptr<KEY1Parser> key1Parser(new KEY1Parser(info.m_input, info.m_package, collector, dict));
467
0
    return key1Parser->parse();
468
0
  }
469
2.56k
  else if (info.m_format == FORMAT_XML2)
470
0
  {
471
0
    KEY2Dictionary dict;
472
0
    shared_ptr<KEY2Parser> key2Parser(new KEY2Parser(info.m_input, info.m_package, collector, dict));
473
0
    return key2Parser->parse();
474
0
  }
475
2.56k
  else if (info.m_format == FORMAT_BINARY)
476
2.49k
  {
477
2.49k
    KEY6Parser parser(info.m_fragments, info.m_package, collector);
478
2.49k
    return parser.parse();
479
2.49k
  }
480
481
73
  ETONYEK_DEBUG_MSG(("EtonyekDocument::parse: unhandled format %d\n", info.m_format));
482
73
  return false;
483
2.56k
}
484
24.9k
catch (...)
485
24.9k
{
486
826
  return false;
487
826
}
488
489
0
ETONYEKAPI bool EtonyekDocument::parse(librevenge::RVNGInputStream *const input, librevenge::RVNGSpreadsheetInterface *const document) try
490
0
{
491
0
  if (!input || !document)
492
0
    return false;
493
494
0
  DetectionInfo info(EtonyekDocument::TYPE_NUMBERS);
495
496
0
  if (!detect(RVNGInputStreamPtr_t(input, EtonyekDummyDeleter()), info))
497
0
    return false;
498
499
0
  info.m_input->seek(0, librevenge::RVNG_SEEK_SET);
500
501
0
  IWORKSpreadsheetRedirector redirector(document);
502
0
  NUMCollector collector(&redirector);
503
0
  if (info.m_format == FORMAT_XML2)
504
0
  {
505
0
    NUM1Dictionary dict;
506
0
    NUM1Parser parser(info.m_input, info.m_package, collector, &dict);
507
0
    return parser.parse();
508
0
  }
509
0
  else if (info.m_format == FORMAT_BINARY)
510
0
  {
511
0
    NUM3Parser parser(info.m_fragments, info.m_package, collector);
512
0
    return parser.parse();
513
0
  }
514
515
0
  ETONYEK_DEBUG_MSG(("EtonyekDocument::parse: unhandled format %d\n", info.m_format));
516
0
  return false;
517
0
}
518
0
catch (...)
519
0
{
520
0
  return false;
521
0
}
522
523
0
ETONYEKAPI bool EtonyekDocument::parse(librevenge::RVNGInputStream *const input, librevenge::RVNGTextInterface *const document) try
524
0
{
525
0
  if (!input || !document)
526
0
    return false;
527
528
0
  DetectionInfo info(EtonyekDocument::TYPE_PAGES);
529
530
0
  if (!detect(RVNGInputStreamPtr_t(input, EtonyekDummyDeleter()), info))
531
0
    return false;
532
533
0
  info.m_input->seek(0, librevenge::RVNG_SEEK_SET);
534
535
0
  IWORKTextRedirector redirector(document);
536
0
  PAGCollector collector(&redirector);
537
0
  if (info.m_format == FORMAT_XML2)
538
0
  {
539
0
    PAG1Dictionary dict;
540
0
    PAG1Parser parser(info.m_input, info.m_package, collector, &dict);
541
0
    return parser.parse();
542
0
  }
543
0
  else if (info.m_format == FORMAT_BINARY)
544
0
  {
545
0
    PAG5Parser parser(info.m_fragments, info.m_package, collector);
546
0
    return parser.parse();
547
0
  }
548
549
0
  ETONYEK_DEBUG_MSG(("EtonyekDocument::parse: unhandled format %d\n", info.m_format));
550
0
  return false;
551
0
}
552
0
catch (...)
553
0
{
554
0
  return false;
555
0
}
556
557
}
558
559
/* vim:set shiftwidth=2 softtabstop=2 expandtab: */