/src/libreoffice/tools/source/stream/strmunx.cxx
Line | Count | Source |
1 | | /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
2 | | /* |
3 | | * This file is part of the LibreOffice 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 | | * This file incorporates work covered by the following license notice: |
10 | | * |
11 | | * Licensed to the Apache Software Foundation (ASF) under one or more |
12 | | * contributor license agreements. See the NOTICE file distributed |
13 | | * with this work for additional information regarding copyright |
14 | | * ownership. The ASF licenses this file to you under the Apache |
15 | | * License, Version 2.0 (the "License"); you may not use this file |
16 | | * except in compliance with the License. You may obtain a copy of |
17 | | * the License at http://www.apache.org/licenses/LICENSE-2.0 . |
18 | | */ |
19 | | |
20 | | #include <stdio.h> |
21 | | #include <fcntl.h> |
22 | | #include <errno.h> |
23 | | |
24 | | #include <tools/stream.hxx> |
25 | | #include <map> |
26 | | |
27 | | #include <mutex> |
28 | | #include <osl/thread.h> |
29 | | #include <sal/log.hxx> |
30 | | |
31 | | #include <osl/file.hxx> |
32 | | #include <osl/detail/file.h> |
33 | | |
34 | | using namespace osl; |
35 | | |
36 | | // InternalLock ---------------------------------------------------------------- |
37 | | |
38 | | namespace { |
39 | | |
40 | | std::mutex& LockMutex() |
41 | 495k | { |
42 | 495k | static std::mutex SINGLETON; |
43 | 495k | return SINGLETON; |
44 | 495k | } |
45 | | |
46 | | std::map<SvFileStream const *, osl::DirectoryItem> gLocks; |
47 | | |
48 | | bool lockFile( const SvFileStream* pStream ) |
49 | 299k | { |
50 | 299k | osl::DirectoryItem aItem; |
51 | 299k | if (osl::DirectoryItem::get( pStream->GetFileName(), aItem) != osl::FileBase::E_None ) |
52 | 299k | { |
53 | 299k | SAL_INFO("tools.stream", "Failed to lookup stream for locking"); |
54 | 299k | return true; |
55 | 299k | } |
56 | | |
57 | 0 | osl::FileStatus aStatus( osl_FileStatus_Mask_Type ); |
58 | 0 | if ( aItem.getFileStatus( aStatus ) != osl::FileBase::E_None ) |
59 | 0 | { |
60 | 0 | SAL_INFO("tools.stream", "Failed to stat stream for locking"); |
61 | 0 | return true; |
62 | 0 | } |
63 | 0 | if( aStatus.getFileType() == osl::FileStatus::Directory ) |
64 | 0 | return true; |
65 | | |
66 | 0 | std::unique_lock aGuard( LockMutex() ); |
67 | 0 | for( const auto& [rLockStream, rLockItem] : gLocks ) |
68 | 0 | { |
69 | 0 | if( aItem.isIdenticalTo( rLockItem ) ) |
70 | 0 | { |
71 | 0 | StreamMode nLockMode = rLockStream->GetStreamMode(); |
72 | 0 | StreamMode nNewMode = pStream->GetStreamMode(); |
73 | 0 | bool bDenyByOptions = (nLockMode & StreamMode::SHARE_DENYALL) || |
74 | 0 | ( (nLockMode & StreamMode::SHARE_DENYWRITE) && (nNewMode & StreamMode::WRITE) ) || |
75 | 0 | ( (nLockMode & StreamMode::SHARE_DENYREAD) && (nNewMode & StreamMode::READ) ); |
76 | |
|
77 | 0 | if( bDenyByOptions ) |
78 | 0 | { |
79 | 0 | return false; // file is already locked |
80 | 0 | } |
81 | 0 | } |
82 | 0 | } |
83 | 0 | gLocks[pStream] = aItem; |
84 | 0 | return true; |
85 | 0 | } |
86 | | |
87 | | void unlockFile( SvFileStream const * pStream ) |
88 | 495k | { |
89 | 495k | std::unique_lock aGuard( LockMutex() ); |
90 | 495k | gLocks.erase(pStream); |
91 | 495k | } |
92 | | |
93 | | } |
94 | | |
95 | | static ErrCode GetSvError( int nErrno ) |
96 | 0 | { |
97 | 0 | static struct { int nErr; ErrCode sv; } const errArr[] = |
98 | 0 | { |
99 | 0 | { 0, ERRCODE_NONE }, |
100 | 0 | { EACCES, SVSTREAM_ACCESS_DENIED }, |
101 | 0 | { EBADF, SVSTREAM_INVALID_HANDLE }, |
102 | | #if defined(NETBSD) || \ |
103 | | defined(FREEBSD) || defined(MACOSX) || defined(OPENBSD) || \ |
104 | | defined(__FreeBSD_kernel__) || defined(DRAGONFLY) || \ |
105 | | defined(IOS) || defined(HAIKU) |
106 | | { EDEADLK, SVSTREAM_LOCKING_VIOLATION }, |
107 | | #else |
108 | 0 | { EDEADLOCK, SVSTREAM_LOCKING_VIOLATION }, |
109 | 0 | #endif |
110 | 0 | { EINVAL, SVSTREAM_INVALID_PARAMETER }, |
111 | 0 | { EMFILE, SVSTREAM_TOO_MANY_OPEN_FILES }, |
112 | 0 | { ENFILE, SVSTREAM_TOO_MANY_OPEN_FILES }, |
113 | 0 | { ENOENT, SVSTREAM_FILE_NOT_FOUND }, |
114 | 0 | { EPERM, SVSTREAM_ACCESS_DENIED }, |
115 | 0 | { EROFS, SVSTREAM_ACCESS_DENIED }, |
116 | 0 | { EAGAIN, SVSTREAM_LOCKING_VIOLATION }, |
117 | 0 | { EISDIR, SVSTREAM_PATH_NOT_FOUND }, |
118 | 0 | { ELOOP, SVSTREAM_PATH_NOT_FOUND }, |
119 | 0 | #if !defined(NETBSD) && !defined (FREEBSD) && \ |
120 | 0 | !defined(MACOSX) && !defined(OPENBSD) && !defined(__FreeBSD_kernel__) && \ |
121 | 0 | !defined(DRAGONFLY) |
122 | 0 | { EMULTIHOP, SVSTREAM_PATH_NOT_FOUND }, |
123 | 0 | { ENOLINK, SVSTREAM_PATH_NOT_FOUND }, |
124 | 0 | #endif |
125 | 0 | { ENOTDIR, SVSTREAM_PATH_NOT_FOUND }, |
126 | 0 | { ETXTBSY, SVSTREAM_ACCESS_DENIED }, |
127 | 0 | { EEXIST, SVSTREAM_CANNOT_MAKE }, |
128 | 0 | { ENOSPC, SVSTREAM_DISK_FULL }, |
129 | 0 | { int(0xFFFF), SVSTREAM_GENERALERROR } |
130 | 0 | }; |
131 | |
|
132 | 0 | ErrCode nRetVal = SVSTREAM_GENERALERROR; // default error |
133 | 0 | int i=0; |
134 | 0 | do |
135 | 0 | { |
136 | 0 | if ( errArr[i].nErr == nErrno ) |
137 | 0 | { |
138 | 0 | nRetVal = errArr[i].sv; |
139 | 0 | break; |
140 | 0 | } |
141 | 0 | ++i; |
142 | 0 | } |
143 | 0 | while( errArr[i].nErr != 0xFFFF ); |
144 | 0 | return nRetVal; |
145 | 0 | } |
146 | | |
147 | | static ErrCode GetSvError( oslFileError nErrno ) |
148 | 4 | { |
149 | 4 | static struct { oslFileError nErr; ErrCode sv; } const errArr[] = |
150 | 4 | { |
151 | 4 | { osl_File_E_None, ERRCODE_NONE }, |
152 | 4 | { osl_File_E_ACCES, SVSTREAM_ACCESS_DENIED }, |
153 | 4 | { osl_File_E_BADF, SVSTREAM_INVALID_HANDLE }, |
154 | 4 | { osl_File_E_DEADLK, SVSTREAM_LOCKING_VIOLATION }, |
155 | 4 | { osl_File_E_INVAL, SVSTREAM_INVALID_PARAMETER }, |
156 | 4 | { osl_File_E_MFILE, SVSTREAM_TOO_MANY_OPEN_FILES }, |
157 | 4 | { osl_File_E_NFILE, SVSTREAM_TOO_MANY_OPEN_FILES }, |
158 | 4 | { osl_File_E_NOENT, SVSTREAM_FILE_NOT_FOUND }, |
159 | 4 | { osl_File_E_PERM, SVSTREAM_ACCESS_DENIED }, |
160 | 4 | { osl_File_E_ROFS, SVSTREAM_ACCESS_DENIED }, |
161 | 4 | { osl_File_E_AGAIN, SVSTREAM_LOCKING_VIOLATION }, |
162 | 4 | { osl_File_E_ISDIR, SVSTREAM_PATH_NOT_FOUND }, |
163 | 4 | { osl_File_E_LOOP, SVSTREAM_PATH_NOT_FOUND }, |
164 | 4 | { osl_File_E_MULTIHOP, SVSTREAM_PATH_NOT_FOUND }, |
165 | 4 | { osl_File_E_NOLINK, SVSTREAM_PATH_NOT_FOUND }, |
166 | 4 | { osl_File_E_NOTDIR, SVSTREAM_PATH_NOT_FOUND }, |
167 | 4 | { osl_File_E_EXIST, SVSTREAM_CANNOT_MAKE }, |
168 | 4 | { osl_File_E_NOSPC, SVSTREAM_DISK_FULL }, |
169 | 4 | { oslFileError(0xFFFF), SVSTREAM_GENERALERROR } |
170 | 4 | }; |
171 | | |
172 | 4 | ErrCode nRetVal = SVSTREAM_GENERALERROR; // default error |
173 | 4 | int i=0; |
174 | 4 | do |
175 | 29 | { |
176 | 29 | if ( errArr[i].nErr == nErrno ) |
177 | 4 | { |
178 | 4 | nRetVal = errArr[i].sv; |
179 | 4 | break; |
180 | 4 | } |
181 | 25 | ++i; |
182 | 25 | } |
183 | 25 | while( errArr[i].nErr != oslFileError(0xFFFF) ); |
184 | 4 | return nRetVal; |
185 | 4 | } |
186 | | |
187 | | SvFileStream::SvFileStream( const OUString& rFileName, StreamMode nOpenMode, std::optional<rtl_TextEncoding> oStreamEncoding ) |
188 | 495k | { |
189 | 495k | if (oStreamEncoding) |
190 | 0 | SetStreamEncoding(*oStreamEncoding); |
191 | 495k | bIsOpen = false; |
192 | 495k | m_isWritable = false; |
193 | | |
194 | 495k | SetBufferSize( 1024 ); |
195 | | // convert URL to SystemPath, if necessary |
196 | 495k | OUString aSystemFileName; |
197 | 495k | if( FileBase::getSystemPathFromFileURL( rFileName , aSystemFileName ) |
198 | 495k | != FileBase::E_None ) |
199 | 1 | { |
200 | 1 | aSystemFileName = rFileName; |
201 | 1 | } |
202 | 495k | Open( aSystemFileName, nOpenMode ); |
203 | 495k | } |
204 | | |
205 | | SvFileStream::SvFileStream() |
206 | 7 | { |
207 | 7 | bIsOpen = false; |
208 | 7 | m_isWritable = false; |
209 | 7 | SetBufferSize( 1024 ); |
210 | 7 | } |
211 | | |
212 | | SvFileStream::~SvFileStream() |
213 | 495k | { |
214 | 495k | Close(); |
215 | 495k | } |
216 | | |
217 | | std::size_t SvFileStream::GetData( void* pData, std::size_t nSize ) |
218 | 42.5M | { |
219 | 42.5M | SAL_INFO("tools", OString::number(static_cast<sal_Int64>(nSize)) << " Bytes from " << aFilename); |
220 | | |
221 | 42.5M | sal_uInt64 nRead = 0; |
222 | 42.5M | if ( IsOpen() ) |
223 | 42.5M | { |
224 | 42.5M | oslFileError rc = osl_readFile(mxFileHandle,pData,static_cast<sal_uInt64>(nSize),&nRead); |
225 | 42.5M | if ( rc != osl_File_E_None ) |
226 | 0 | { |
227 | 0 | SetError( ::GetSvError( rc )); |
228 | 0 | return -1; |
229 | 0 | } |
230 | 42.5M | } |
231 | 42.5M | return static_cast<std::size_t>(nRead); |
232 | 42.5M | } |
233 | | |
234 | | std::size_t SvFileStream::PutData( const void* pData, std::size_t nSize ) |
235 | 1.11M | { |
236 | 1.11M | SAL_INFO("tools", OString::number(static_cast<sal_Int64>(nSize)) << " Bytes to " << aFilename); |
237 | | |
238 | 1.11M | sal_uInt64 nWrite = 0; |
239 | 1.11M | if ( IsOpen() ) |
240 | 1.11M | { |
241 | 1.11M | oslFileError rc = osl_writeFile(mxFileHandle,pData,static_cast<sal_uInt64>(nSize),&nWrite); |
242 | 1.11M | if ( rc != osl_File_E_None ) |
243 | 0 | { |
244 | 0 | SetError( ::GetSvError( rc ) ); |
245 | 0 | return -1; |
246 | 0 | } |
247 | 1.11M | else if( !nWrite ) |
248 | 0 | { |
249 | 0 | SetError( SVSTREAM_DISK_FULL ); |
250 | 0 | return -1; |
251 | 0 | } |
252 | 1.11M | } |
253 | 1.11M | return static_cast<std::size_t>(nWrite); |
254 | 1.11M | } |
255 | | |
256 | | sal_uInt64 SvFileStream::SeekPos(sal_uInt64 const nPos) |
257 | 71.8M | { |
258 | | // check if a truncated STREAM_SEEK_TO_END was passed |
259 | 71.8M | assert(nPos != sal_uInt64(sal_uInt32(STREAM_SEEK_TO_END))); |
260 | 71.8M | if ( IsOpen() ) |
261 | 71.8M | { |
262 | 71.8M | oslFileError rc; |
263 | 71.8M | sal_uInt64 nNewPos; |
264 | 71.8M | if ( nPos != STREAM_SEEK_TO_END ) |
265 | 55.4M | rc = osl_setFilePos( mxFileHandle, osl_Pos_Absolut, nPos ); |
266 | 16.3M | else |
267 | 16.3M | rc = osl_setFilePos( mxFileHandle, osl_Pos_End, 0 ); |
268 | | |
269 | 71.8M | if ( rc != osl_File_E_None ) |
270 | 0 | { |
271 | 0 | SetError( SVSTREAM_SEEK_ERROR ); |
272 | 0 | return 0; |
273 | 0 | } |
274 | 71.8M | if ( nPos != STREAM_SEEK_TO_END ) |
275 | 55.4M | return nPos; |
276 | 16.3M | osl_getFilePos( mxFileHandle, &nNewPos ); |
277 | 16.3M | return nNewPos; |
278 | 71.8M | } |
279 | 0 | SetError( SVSTREAM_GENERALERROR ); |
280 | 0 | return 0; |
281 | 71.8M | } |
282 | | |
283 | | void SvFileStream::FlushData() |
284 | 327k | { |
285 | 327k | auto rc = osl_syncFile(mxFileHandle); |
286 | 327k | if (rc != osl_File_E_None) |
287 | 0 | SetError( ::GetSvError( rc )); |
288 | 327k | } |
289 | | |
290 | | bool SvFileStream::LockFile() |
291 | 495k | { |
292 | 495k | int nLockMode = 0; |
293 | | |
294 | 495k | if ( ! IsOpen() ) |
295 | 0 | return false; |
296 | | |
297 | 495k | if (m_eStreamMode & StreamMode::SHARE_DENYALL) |
298 | 0 | { |
299 | 0 | if (m_isWritable) |
300 | 0 | nLockMode = F_WRLCK; |
301 | 0 | else |
302 | 0 | nLockMode = F_RDLCK; |
303 | 0 | } |
304 | | |
305 | 495k | if (m_eStreamMode & StreamMode::SHARE_DENYREAD) |
306 | 0 | { |
307 | 0 | if (m_isWritable) |
308 | 0 | nLockMode = F_WRLCK; |
309 | 0 | else |
310 | 0 | { |
311 | 0 | SetError(SVSTREAM_LOCKING_VIOLATION); |
312 | 0 | return false; |
313 | 0 | } |
314 | 0 | } |
315 | | |
316 | 495k | if (m_eStreamMode & StreamMode::SHARE_DENYWRITE) |
317 | 299k | { |
318 | 299k | if (m_isWritable) |
319 | 299k | nLockMode = F_WRLCK; |
320 | 0 | else |
321 | 0 | nLockMode = F_RDLCK; |
322 | 299k | } |
323 | | |
324 | 495k | if (!nLockMode) |
325 | 196k | return true; |
326 | | |
327 | 299k | if( !lockFile( this ) ) |
328 | 0 | { |
329 | 0 | SAL_WARN("tools.stream", "InternalLock on " << aFilename << "failed"); |
330 | 0 | return false; |
331 | 0 | } |
332 | | |
333 | 299k | return true; |
334 | 299k | } |
335 | | |
336 | | void SvFileStream::UnlockFile() |
337 | 1.00M | { |
338 | 1.00M | if ( ! IsOpen() ) |
339 | 506k | return; |
340 | | |
341 | 495k | unlockFile( this ); |
342 | 495k | } |
343 | | |
344 | | void SvFileStream::Open( const OUString& rFilename, StreamMode nOpenMode ) |
345 | 495k | { |
346 | 495k | sal_uInt32 uFlags; |
347 | 495k | oslFileHandle nHandleTmp; |
348 | | |
349 | 495k | Close(); |
350 | 495k | errno = 0; |
351 | 495k | m_eStreamMode = nOpenMode; |
352 | 495k | m_eStreamMode &= ~StreamMode::TRUNC; // don't truncate on reopen |
353 | | |
354 | 495k | aFilename = rFilename; |
355 | | |
356 | 495k | SAL_INFO("tools", aFilename); |
357 | | |
358 | 495k | OUString aFileURL; |
359 | 495k | osl::DirectoryItem aItem; |
360 | 495k | osl::FileStatus aStatus( osl_FileStatus_Mask_Type | osl_FileStatus_Mask_LinkTargetURL ); |
361 | | |
362 | | // FIXME: we really need to switch to a pure URL model ... |
363 | 495k | if ( osl::File::getFileURLFromSystemPath( aFilename, aFileURL ) != osl::FileBase::E_None ) |
364 | 0 | aFileURL = aFilename; |
365 | | |
366 | | // don't both stat()ing a temporary file, unnecessary |
367 | 495k | bool bStatValid = true; |
368 | 495k | if (!(nOpenMode & StreamMode::TEMPORARY)) |
369 | 10.5k | { |
370 | 10.5k | bStatValid = ( osl::DirectoryItem::get( aFileURL, aItem) == osl::FileBase::E_None && |
371 | 10.5k | aItem.getFileStatus( aStatus ) == osl::FileBase::E_None ); |
372 | | |
373 | | // SvFileStream can't open a directory |
374 | 10.5k | if( bStatValid && aStatus.getFileType() == osl::FileStatus::Directory ) |
375 | 0 | { |
376 | 0 | SetError( ::GetSvError( EISDIR ) ); |
377 | 0 | return; |
378 | 0 | } |
379 | 10.5k | } |
380 | | |
381 | 495k | if ( !( nOpenMode & StreamMode::WRITE ) ) |
382 | 4 | uFlags = osl_File_OpenFlag_Read; |
383 | 495k | else if ( !( nOpenMode & StreamMode::READ ) ) |
384 | 10.3k | uFlags = osl_File_OpenFlag_Write; |
385 | 485k | else |
386 | 485k | uFlags = osl_File_OpenFlag_Read | osl_File_OpenFlag_Write; |
387 | | |
388 | | // Fix (MDA, 18.01.95): Don't open with O_CREAT upon RD_ONLY |
389 | | // Important for Read-Only-Filesystems (e.g, CDROM) |
390 | 495k | if ( (!( nOpenMode & StreamMode::NOCREATE )) && ( uFlags != osl_File_OpenFlag_Read ) ) |
391 | 495k | uFlags |= osl_File_OpenFlag_Create; |
392 | 495k | if ( nOpenMode & StreamMode::TRUNC ) |
393 | 0 | uFlags |= osl_File_OpenFlag_Trunc; |
394 | | |
395 | 495k | uFlags |= osl_File_OpenFlag_NoExcl | osl_File_OpenFlag_NoLock; |
396 | | |
397 | 495k | if ( nOpenMode & StreamMode::WRITE) |
398 | 495k | { |
399 | 495k | if ( nOpenMode & StreamMode::COPY_ON_SYMLINK ) |
400 | 0 | { |
401 | 0 | if ( bStatValid && aStatus.getFileType() == osl::FileStatus::Link && |
402 | 0 | aStatus.getLinkTargetURL().getLength() > 0 ) |
403 | 0 | { |
404 | | // delete the symbolic link, and replace it with the contents of the link |
405 | 0 | if (osl::File::remove( aFileURL ) == osl::FileBase::E_None ) |
406 | 0 | { |
407 | 0 | File::copy( aStatus.getLinkTargetURL(), aFileURL ); |
408 | 0 | SAL_INFO("tools.stream", |
409 | 0 | "Removing link and replacing with file contents (" << |
410 | 0 | aStatus.getLinkTargetURL() << ") -> (" << aFileURL << ")."); |
411 | 0 | } |
412 | 0 | } |
413 | 0 | } |
414 | 495k | } |
415 | | |
416 | 495k | oslFileError rc = osl_openFile( aFileURL.pData, &nHandleTmp, uFlags ); |
417 | 495k | if ( rc != osl_File_E_None ) |
418 | 4 | { |
419 | 4 | if ( uFlags & osl_File_OpenFlag_Write ) |
420 | 0 | { |
421 | | // Change to read-only |
422 | 0 | uFlags &= ~osl_File_OpenFlag_Write; |
423 | 0 | rc = osl_openFile( aFileURL.pData, &nHandleTmp, uFlags ); |
424 | 0 | } |
425 | 4 | } |
426 | 495k | if ( rc == osl_File_E_None ) |
427 | 495k | { |
428 | 495k | mxFileHandle = nHandleTmp; |
429 | 495k | bIsOpen = true; |
430 | 495k | if ( uFlags & osl_File_OpenFlag_Write ) |
431 | 495k | m_isWritable = true; |
432 | | |
433 | 495k | if ( !LockFile() ) // whole file |
434 | 0 | { |
435 | 0 | osl_closeFile( nHandleTmp ); |
436 | 0 | bIsOpen = false; |
437 | 0 | m_isWritable = false; |
438 | 0 | mxFileHandle = nullptr; |
439 | 0 | } |
440 | 495k | } |
441 | 4 | else |
442 | 4 | SetError( ::GetSvError( rc ) ); |
443 | 495k | } |
444 | | |
445 | | void SvFileStream::Close() |
446 | 1.00M | { |
447 | 1.00M | UnlockFile(); |
448 | | |
449 | 1.00M | if ( IsOpen() ) |
450 | 495k | { |
451 | 495k | SAL_INFO("tools", "Closing " << aFilename); |
452 | 495k | FlushBuffer(); |
453 | 495k | osl_closeFile( mxFileHandle ); |
454 | 495k | mxFileHandle = nullptr; |
455 | 495k | } |
456 | | |
457 | 1.00M | bIsOpen = false; |
458 | 1.00M | m_isWritable = false; |
459 | 1.00M | SvStream::ClearBuffer(); |
460 | 1.00M | SvStream::ClearError(); |
461 | 1.00M | } |
462 | | |
463 | | /// set filepointer to beginning of file |
464 | | void SvFileStream::ResetError() |
465 | 5.88k | { |
466 | 5.88k | SvStream::ClearError(); |
467 | 5.88k | } |
468 | | |
469 | | void SvFileStream::SetSize (sal_uInt64 const nSize) |
470 | 136 | { |
471 | 136 | if (IsOpen()) |
472 | 136 | { |
473 | 136 | oslFileError rc = osl_setFileSize( mxFileHandle, nSize ); |
474 | 136 | if (rc != osl_File_E_None ) |
475 | 0 | { |
476 | 0 | SetError ( ::GetSvError( rc )); |
477 | 0 | } |
478 | 136 | } |
479 | 136 | } |
480 | | |
481 | | /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |