/src/libstaroffice/src/lib/STOFFDocument.cxx
Line | Count | Source |
1 | | /* -*- Mode: C++; c-default-style: "k&r"; indent-tabs-mode: nil; tab-width: 2; c-basic-offset: 2 -*- */ |
2 | | |
3 | | /* libstaroffice |
4 | | * Version: MPL 2.0 / LGPLv2+ |
5 | | * |
6 | | * The contents of this file are subject to the Mozilla Public License Version |
7 | | * 2.0 (the "License"); you may not use this file except in compliance with |
8 | | * the License or as specified alternatively below. You may obtain a copy of |
9 | | * the License at http://www.mozilla.org/MPL/ |
10 | | * |
11 | | * Software distributed under the License is distributed on an "AS IS" basis, |
12 | | * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License |
13 | | * for the specific language governing rights and limitations under the |
14 | | * License. |
15 | | * |
16 | | * Major Contributor(s): |
17 | | * Copyright (C) 2002 William Lachance (wrlach@gmail.com) |
18 | | * Copyright (C) 2002,2004 Marc Maurer (uwog@uwog.net) |
19 | | * Copyright (C) 2004-2006 Fridrich Strba (fridrich.strba@bluewin.ch) |
20 | | * Copyright (C) 2006, 2007 Andrew Ziem |
21 | | * Copyright (C) 2011, 2012 Alonso Laurent (alonso@loria.fr) |
22 | | * |
23 | | * |
24 | | * All Rights Reserved. |
25 | | * |
26 | | * For minor contributions see the git repository. |
27 | | * |
28 | | * Alternatively, the contents of this file may be used under the terms of |
29 | | * the GNU Lesser General Public License Version 2 or later (the "LGPLv2+"), |
30 | | * in which case the provisions of the LGPLv2+ are applicable |
31 | | * instead of those above. |
32 | | */ |
33 | | |
34 | | /** \file STOFFDocument.cxx |
35 | | * libstoff API: implementation of main interface functions |
36 | | */ |
37 | | |
38 | | #include "SDAParser.hxx" |
39 | | #include "SDCParser.hxx" |
40 | | #include "SDGParser.hxx" |
41 | | #include "SDWParser.hxx" |
42 | | #include "SDXParser.hxx" |
43 | | |
44 | | #include "STOFFHeader.hxx" |
45 | | #include "STOFFGraphicDecoder.hxx" |
46 | | #include "STOFFParser.hxx" |
47 | | #include "STOFFPropertyHandler.hxx" |
48 | | #include "STOFFSpreadsheetDecoder.hxx" |
49 | | |
50 | | #include <libstaroffice/libstaroffice.hxx> |
51 | | |
52 | | /** small namespace use to define private class/method used by STOFFDocument */ |
53 | | namespace STOFFDocumentInternal |
54 | | { |
55 | | std::shared_ptr<STOFFGraphicParser> getGraphicParserFromHeader(STOFFInputStreamPtr &input, STOFFHeader *header, char const *passwd); |
56 | | std::shared_ptr<STOFFGraphicParser> getPresentationParserFromHeader(STOFFInputStreamPtr &input, STOFFHeader *header, char const *passwd); |
57 | | std::shared_ptr<STOFFTextParser> getTextParserFromHeader(STOFFInputStreamPtr &input, STOFFHeader *header, char const *passwd); |
58 | | std::shared_ptr<STOFFSpreadsheetParser> getSpreadsheetParserFromHeader(STOFFInputStreamPtr &input, STOFFHeader *header, char const *passwd); |
59 | | STOFFHeader *getHeader(STOFFInputStreamPtr &input, bool strict); |
60 | | bool checkHeader(STOFFInputStreamPtr &input, STOFFHeader &header, bool strict); |
61 | | } |
62 | | |
63 | | STOFFDocument::Confidence STOFFDocument::isFileFormatSupported(librevenge::RVNGInputStream *input, Kind &kind) |
64 | 0 | try |
65 | 0 | { |
66 | 0 | kind = STOFF_K_UNKNOWN; |
67 | |
|
68 | 0 | if (!input) { |
69 | 0 | STOFF_DEBUG_MSG(("STOFFDocument::isFileFormatSupported(): no input\n")); |
70 | 0 | return STOFF_C_NONE; |
71 | 0 | } |
72 | | |
73 | 0 | STOFFInputStreamPtr ip(new STOFFInputStream(input, false)); |
74 | 0 | std::shared_ptr<STOFFHeader> header; |
75 | | #ifdef DEBUG |
76 | | header.reset(STOFFDocumentInternal::getHeader(ip, false)); |
77 | | #else |
78 | 0 | header.reset(STOFFDocumentInternal::getHeader(ip, true)); |
79 | 0 | #endif |
80 | |
|
81 | 0 | if (!header.get()) |
82 | 0 | return STOFF_C_NONE; |
83 | 0 | kind = static_cast<STOFFDocument::Kind>(header->getKind()); |
84 | 0 | return header->isEncrypted() ? STOFF_C_SUPPORTED_ENCRYPTION : STOFF_C_EXCELLENT; |
85 | 0 | } |
86 | 0 | catch (...) |
87 | 0 | { |
88 | 0 | STOFF_DEBUG_MSG(("STOFFDocument::isFileFormatSupported: exception catched\n")); |
89 | 0 | kind = STOFF_K_UNKNOWN; |
90 | 0 | return STOFF_C_NONE; |
91 | 0 | } |
92 | | |
93 | | STOFFDocument::Result STOFFDocument::parse(librevenge::RVNGInputStream *input, librevenge::RVNGDrawingInterface *documentInterface, char const *password) |
94 | 24.3k | try |
95 | 24.3k | { |
96 | 24.3k | if (!input) |
97 | 0 | return STOFF_R_UNKNOWN_ERROR; |
98 | | |
99 | 24.3k | STOFFInputStreamPtr ip(new STOFFInputStream(input, false)); |
100 | 24.3k | std::shared_ptr<STOFFHeader> header(STOFFDocumentInternal::getHeader(ip, false)); |
101 | | |
102 | 24.3k | if (!header.get()) return STOFF_R_UNKNOWN_ERROR; |
103 | 22.4k | auto parser=STOFFDocumentInternal::getGraphicParserFromHeader(ip, header.get(), password); |
104 | 22.4k | if (!parser) return STOFF_R_UNKNOWN_ERROR; |
105 | 22.3k | parser->parse(documentInterface); |
106 | 22.3k | return STOFF_R_OK; |
107 | 22.4k | } |
108 | 24.3k | catch (libstoff::FileException) |
109 | 24.3k | { |
110 | 0 | STOFF_DEBUG_MSG(("STOFFDocument::parse: File exception trapped\n")); |
111 | 0 | return STOFF_R_FILE_ACCESS_ERROR; |
112 | 0 | } |
113 | 24.3k | catch (libstoff::ParseException) |
114 | 24.3k | { |
115 | 2.98k | STOFF_DEBUG_MSG(("STOFFDocument::parse: Parse exception trapped\n")); |
116 | 2.98k | return STOFF_R_PARSE_ERROR; |
117 | 2.98k | } |
118 | 24.3k | catch (libstoff::WrongPasswordException) |
119 | 24.3k | { |
120 | 0 | STOFF_DEBUG_MSG(("STOFFDocument::parse: Parse password trapped\n")); |
121 | 0 | return STOFF_R_PASSWORD_MISSMATCH_ERROR; |
122 | 0 | } |
123 | 24.3k | catch (...) |
124 | 24.3k | { |
125 | | //fixme: too generic |
126 | 0 | STOFF_DEBUG_MSG(("STOFFDocument::parse: Unknown exception trapped\n")); |
127 | 0 | return STOFF_R_UNKNOWN_ERROR; |
128 | 0 | } |
129 | | |
130 | | STOFFDocument::Result STOFFDocument::parse(librevenge::RVNGInputStream *input, librevenge::RVNGPresentationInterface *documentInterface, char const *password) |
131 | 1.98k | try |
132 | 1.98k | { |
133 | 1.98k | if (!input) |
134 | 0 | return STOFF_R_UNKNOWN_ERROR; |
135 | | |
136 | 1.98k | STOFFInputStreamPtr ip(new STOFFInputStream(input, false)); |
137 | 1.98k | std::shared_ptr<STOFFHeader> header(STOFFDocumentInternal::getHeader(ip, false)); |
138 | | |
139 | 1.98k | if (!header.get()) return STOFF_R_UNKNOWN_ERROR; |
140 | 1.87k | auto parser=STOFFDocumentInternal::getPresentationParserFromHeader(ip, header.get(), password); |
141 | 1.87k | if (!parser) return STOFF_R_UNKNOWN_ERROR; |
142 | 1.85k | parser->parse(documentInterface); |
143 | 1.85k | return STOFF_R_OK; |
144 | 1.87k | } |
145 | 1.98k | catch (libstoff::FileException) |
146 | 1.98k | { |
147 | 0 | STOFF_DEBUG_MSG(("STOFFDocument::parse: File exception trapped\n")); |
148 | 0 | return STOFF_R_FILE_ACCESS_ERROR; |
149 | 0 | } |
150 | 1.98k | catch (libstoff::ParseException) |
151 | 1.98k | { |
152 | 12 | STOFF_DEBUG_MSG(("STOFFDocument::parse: Parse exception trapped\n")); |
153 | 12 | return STOFF_R_PARSE_ERROR; |
154 | 12 | } |
155 | 1.98k | catch (libstoff::WrongPasswordException) |
156 | 1.98k | { |
157 | 0 | STOFF_DEBUG_MSG(("STOFFDocument::parse: Parse password trapped\n")); |
158 | 0 | return STOFF_R_PASSWORD_MISSMATCH_ERROR; |
159 | 0 | } |
160 | 1.98k | catch (...) |
161 | 1.98k | { |
162 | | //fixme: too generic |
163 | 0 | STOFF_DEBUG_MSG(("STOFFDocument::parse: Unknown exception trapped\n")); |
164 | 0 | return STOFF_R_UNKNOWN_ERROR; |
165 | 0 | } |
166 | | |
167 | | STOFFDocument::Result STOFFDocument::parse(librevenge::RVNGInputStream *input, librevenge::RVNGSpreadsheetInterface *documentInterface, char const *password) |
168 | 9.84k | try |
169 | 9.84k | { |
170 | 9.84k | if (!input) |
171 | 0 | return STOFF_R_UNKNOWN_ERROR; |
172 | | |
173 | 9.84k | STOFFInputStreamPtr ip(new STOFFInputStream(input, false)); |
174 | 9.84k | std::shared_ptr<STOFFHeader> header(STOFFDocumentInternal::getHeader(ip, false)); |
175 | | |
176 | 9.84k | if (!header.get()) return STOFF_R_UNKNOWN_ERROR; |
177 | 9.51k | auto parser=STOFFDocumentInternal::getSpreadsheetParserFromHeader(ip, header.get(), password); |
178 | 9.51k | if (!parser) return STOFF_R_UNKNOWN_ERROR; |
179 | 9.33k | parser->parse(documentInterface); |
180 | 9.33k | return STOFF_R_OK; |
181 | 9.51k | } |
182 | 9.84k | catch (libstoff::FileException) |
183 | 9.84k | { |
184 | 0 | STOFF_DEBUG_MSG(("STOFFDocument::parse: File exception trapped\n")); |
185 | 0 | return STOFF_R_FILE_ACCESS_ERROR; |
186 | 0 | } |
187 | 9.84k | catch (libstoff::ParseException) |
188 | 9.84k | { |
189 | 46 | STOFF_DEBUG_MSG(("STOFFDocument::parse: Parse exception trapped\n")); |
190 | 46 | return STOFF_R_PARSE_ERROR; |
191 | 46 | } |
192 | 9.84k | catch (libstoff::WrongPasswordException) |
193 | 9.84k | { |
194 | 0 | STOFF_DEBUG_MSG(("STOFFDocument::parse: Parse password trapped\n")); |
195 | 0 | return STOFF_R_PASSWORD_MISSMATCH_ERROR; |
196 | 0 | } |
197 | 9.84k | catch (...) |
198 | 9.84k | { |
199 | | //fixme: too generic |
200 | 0 | STOFF_DEBUG_MSG(("STOFFDocument::parse: Unknown exception trapped\n")); |
201 | 0 | return STOFF_R_UNKNOWN_ERROR; |
202 | 0 | } |
203 | | |
204 | | STOFFDocument::Result STOFFDocument::parse(librevenge::RVNGInputStream *input, librevenge::RVNGTextInterface *documentInterface, char const *password) |
205 | 25.6k | try |
206 | 25.6k | { |
207 | 25.6k | if (!input) |
208 | 0 | return STOFF_R_UNKNOWN_ERROR; |
209 | | |
210 | 25.6k | STOFFInputStreamPtr ip(new STOFFInputStream(input, false)); |
211 | 25.6k | std::shared_ptr<STOFFHeader> header(STOFFDocumentInternal::getHeader(ip, false)); |
212 | | |
213 | 25.6k | if (!header.get()) return STOFF_R_UNKNOWN_ERROR; |
214 | 23.8k | auto parser=STOFFDocumentInternal::getTextParserFromHeader(ip, header.get(), password); |
215 | 23.8k | if (!parser) return STOFF_R_UNKNOWN_ERROR; |
216 | 23.4k | parser->parse(documentInterface); |
217 | | |
218 | 23.4k | return STOFF_R_OK; |
219 | 23.8k | } |
220 | 25.6k | catch (libstoff::FileException) |
221 | 25.6k | { |
222 | 0 | STOFF_DEBUG_MSG(("STOFFDocument::parse: File exception trapped\n")); |
223 | 0 | return STOFF_R_FILE_ACCESS_ERROR; |
224 | 0 | } |
225 | 25.6k | catch (libstoff::ParseException) |
226 | 25.6k | { |
227 | 158 | STOFF_DEBUG_MSG(("STOFFDocument::parse: Parse exception trapped\n")); |
228 | 158 | return STOFF_R_PARSE_ERROR; |
229 | 158 | } |
230 | 25.6k | catch (libstoff::WrongPasswordException) |
231 | 25.6k | { |
232 | 0 | STOFF_DEBUG_MSG(("STOFFDocument::parse: Parse password trapped\n")); |
233 | 0 | return STOFF_R_PASSWORD_MISSMATCH_ERROR; |
234 | 0 | } |
235 | 25.6k | catch (...) |
236 | 25.6k | { |
237 | | //fixme: too generic |
238 | 0 | STOFF_DEBUG_MSG(("STOFFDocument::parse: Unknown exception trapped\n")); |
239 | 0 | return STOFF_R_UNKNOWN_ERROR; |
240 | 0 | } |
241 | | |
242 | | bool STOFFDocument::decodeGraphic(librevenge::RVNGBinaryData const &binary, librevenge::RVNGDrawingInterface *paintInterface) |
243 | 0 | try |
244 | 0 | { |
245 | 0 | if (!paintInterface || !binary.size()) { |
246 | 0 | STOFF_DEBUG_MSG(("STOFFDocument::decodeGraphic: called with no data or no converter\n")); |
247 | 0 | return false; |
248 | 0 | } |
249 | 0 | STOFFGraphicDecoder tmpHandler(paintInterface); |
250 | 0 | if (!tmpHandler.checkData(binary) || !tmpHandler.readData(binary)) return false; |
251 | 0 | return true; |
252 | 0 | } |
253 | 0 | catch (...) |
254 | 0 | { |
255 | 0 | STOFF_DEBUG_MSG(("STOFFDocument::decodeGraphic: unknown error\n")); |
256 | 0 | return false; |
257 | 0 | } |
258 | | |
259 | | bool STOFFDocument::decodeSpreadsheet(librevenge::RVNGBinaryData const &binary, librevenge::RVNGSpreadsheetInterface *sheetInterface) |
260 | 0 | try |
261 | 0 | { |
262 | 0 | if (!sheetInterface || !binary.size()) { |
263 | 0 | STOFF_DEBUG_MSG(("STOFFDocument::decodeSpreadsheet: called with no data or no converter\n")); |
264 | 0 | return false; |
265 | 0 | } |
266 | 0 | STOFFSpreadsheetDecoder tmpHandler(sheetInterface); |
267 | 0 | if (!tmpHandler.checkData(binary) || !tmpHandler.readData(binary)) return false; |
268 | 0 | return true; |
269 | 0 | } |
270 | 0 | catch (...) |
271 | 0 | { |
272 | 0 | STOFF_DEBUG_MSG(("STOFFDocument::decodeSpreadsheet: unknown error\n")); |
273 | 0 | return false; |
274 | 0 | } |
275 | | |
276 | | bool STOFFDocument::decodeText(librevenge::RVNGBinaryData const &, librevenge::RVNGTextInterface *) |
277 | 0 | { |
278 | 0 | STOFF_DEBUG_MSG(("STOFFDocument::decodeText: unimplemented\n")); |
279 | 0 | return false; |
280 | 0 | } |
281 | | |
282 | | namespace STOFFDocumentInternal |
283 | | { |
284 | | /** return the header corresponding to an input. Or 0L if no input are found */ |
285 | | STOFFHeader *getHeader(STOFFInputStreamPtr &ip, bool strict) |
286 | 61.8k | try |
287 | 61.8k | { |
288 | 61.8k | if (!ip.get()) return nullptr; |
289 | | |
290 | 61.8k | if (ip->size() < 10) return nullptr; |
291 | | |
292 | 61.8k | ip->seek(0, librevenge::RVNG_SEEK_SET); |
293 | 61.8k | ip->setReadInverted(false); |
294 | | |
295 | 61.8k | auto listHeaders = STOFFHeader::constructHeader(ip); |
296 | 61.8k | for (auto &h : listHeaders) { |
297 | 57.7k | if (!STOFFDocumentInternal::checkHeader(ip, h, strict)) |
298 | 155 | continue; |
299 | 57.6k | return new STOFFHeader(h); |
300 | 57.7k | } |
301 | 4.17k | return nullptr; |
302 | 61.8k | } |
303 | 61.8k | catch (libstoff::FileException) |
304 | 61.8k | { |
305 | 0 | STOFF_DEBUG_MSG(("STOFFDocumentInternal::STOFFDocument[getHeader]:File exception trapped\n")); |
306 | 0 | return nullptr; |
307 | 0 | } |
308 | 61.8k | catch (libstoff::ParseException) |
309 | 61.8k | { |
310 | 0 | STOFF_DEBUG_MSG(("STOFFDocumentInternal::getHeader:Parse exception trapped\n")); |
311 | 0 | return nullptr; |
312 | 0 | } |
313 | 61.8k | catch (...) |
314 | 61.8k | { |
315 | | //fixme: too generic |
316 | 0 | STOFF_DEBUG_MSG(("STOFFDocumentInternal::getHeader:Unknown exception trapped\n")); |
317 | 0 | return nullptr; |
318 | 0 | } |
319 | | |
320 | | /** Factory wrapper to construct a parser corresponding to an graphic header */ |
321 | | std::shared_ptr<STOFFGraphicParser> getGraphicParserFromHeader(STOFFInputStreamPtr &input, STOFFHeader *header, char const *passwd) |
322 | 47.3k | { |
323 | 47.3k | std::shared_ptr<STOFFGraphicParser> parser; |
324 | 47.3k | if (!header || (header->getKind()!=STOFFDocument::STOFF_K_DRAW && header->getKind()!=STOFFDocument::STOFF_K_GRAPHIC)) |
325 | 210 | return parser; |
326 | 47.1k | try { |
327 | 47.1k | if (header->getKind()==STOFFDocument::STOFF_K_DRAW) { |
328 | 40.9k | SDAParser *sdaParser=new SDAParser(input, header); |
329 | 40.9k | parser.reset(sdaParser); |
330 | 40.9k | if (passwd) sdaParser->setDocumentPassword(passwd); |
331 | 40.9k | } |
332 | 6.16k | else { |
333 | 6.16k | SDGParser *sdgParser=new SDGParser(input, header); |
334 | 6.16k | parser.reset(sdgParser); |
335 | 6.16k | if (passwd) sdgParser->setDocumentPassword(passwd); |
336 | 6.16k | } |
337 | 47.1k | } |
338 | 47.1k | catch (...) { |
339 | 0 | } |
340 | 47.1k | return parser; |
341 | 47.1k | } |
342 | | |
343 | | /** Factory wrapper to construct a parser corresponding to an presentation header */ |
344 | | std::shared_ptr<STOFFGraphicParser> getPresentationParserFromHeader(STOFFInputStreamPtr &input, STOFFHeader *header, char const *passwd) |
345 | 1.87k | { |
346 | 1.87k | std::shared_ptr<STOFFGraphicParser> parser; |
347 | 1.87k | if (!header || header->getKind()!=STOFFDocument::STOFF_K_PRESENTATION) |
348 | 16 | return parser; |
349 | 1.85k | try { |
350 | 1.85k | SDAParser *sdaParser=new SDAParser(input, header); |
351 | 1.85k | parser.reset(sdaParser); |
352 | 1.85k | if (passwd) sdaParser->setDocumentPassword(passwd); |
353 | 1.85k | } |
354 | 1.85k | catch (...) { |
355 | 0 | } |
356 | 1.85k | return parser; |
357 | 1.85k | } |
358 | | |
359 | | /** Factory wrapper to construct a parser corresponding to an text header */ |
360 | | std::shared_ptr<STOFFTextParser> getTextParserFromHeader(STOFFInputStreamPtr &input, STOFFHeader *header, char const *passwd) |
361 | 81.6k | { |
362 | 81.6k | std::shared_ptr<STOFFTextParser> parser; |
363 | 81.6k | if (!header) |
364 | 0 | return parser; |
365 | 81.6k | if (header->getKind()==STOFFDocument::STOFF_K_TEXT) { |
366 | 46.9k | try { |
367 | 46.9k | SDWParser *sdwParser=new SDWParser(input, header); |
368 | 46.9k | parser.reset(sdwParser); |
369 | 46.9k | if (passwd) sdwParser->setDocumentPassword(passwd); |
370 | 46.9k | return parser; |
371 | 46.9k | } |
372 | 46.9k | catch (...) { |
373 | 0 | } |
374 | 46.9k | } |
375 | | #ifdef DEBUG |
376 | | if (header->getKind()==STOFFDocument::STOFF_K_SPREADSHEET || header->getKind()==STOFFDocument::STOFF_K_DRAW || header->getKind()==STOFFDocument::STOFF_K_GRAPHIC) |
377 | | return parser; |
378 | | try { |
379 | | SDXParser *sdxParser=new SDXParser(input, header); |
380 | | parser.reset(sdxParser); |
381 | | if (passwd) sdxParser->setDocumentPassword(passwd); |
382 | | } |
383 | | catch (...) { |
384 | | } |
385 | | #endif |
386 | 34.6k | return parser; |
387 | 81.6k | } |
388 | | |
389 | | /** Factory wrapper to construct a parser corresponding to an spreadsheet header */ |
390 | | std::shared_ptr<STOFFSpreadsheetParser> getSpreadsheetParserFromHeader(STOFFInputStreamPtr &input, STOFFHeader *header, char const *passwd) |
391 | 43.7k | { |
392 | 43.7k | std::shared_ptr<STOFFSpreadsheetParser> parser; |
393 | 43.7k | if (!header || header->getKind()!=STOFFDocument::STOFF_K_SPREADSHEET) |
394 | 25.0k | return parser; |
395 | 18.7k | try { |
396 | 18.7k | SDCParser *sdcParser=new SDCParser(input, header); |
397 | 18.7k | parser.reset(sdcParser); |
398 | 18.7k | if (passwd) sdcParser->setDocumentPassword(passwd); |
399 | 18.7k | } |
400 | 18.7k | catch (...) { |
401 | 0 | } |
402 | 18.7k | return parser; |
403 | 18.7k | } |
404 | | |
405 | | /** Wrapper to check a basic header of a mac file */ |
406 | | bool checkHeader(STOFFInputStreamPtr &input, STOFFHeader &header, bool strict) |
407 | 57.7k | try |
408 | 57.7k | { |
409 | 57.7k | std::shared_ptr<STOFFParser> parser=getTextParserFromHeader(input, &header, nullptr); |
410 | 57.7k | if (!parser) parser=getSpreadsheetParserFromHeader(input, &header, nullptr); |
411 | 57.7k | if (!parser) parser=getGraphicParserFromHeader(input, &header, nullptr); |
412 | 57.7k | if (!parser) return false; |
413 | 57.6k | return parser->checkHeader(&header, strict); |
414 | 57.7k | } |
415 | 57.7k | catch (...) |
416 | 57.7k | { |
417 | 0 | STOFF_DEBUG_MSG(("STOFFDocumentInternal::checkHeader:Unknown exception trapped\n")); |
418 | 0 | return false; |
419 | 0 | } |
420 | | |
421 | | } |
422 | | // vim: set filetype=cpp tabstop=2 shiftwidth=2 cindent autoindent smartindent noexpandtab: |