1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
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/.
9 * This file incorporates work covered by the following license notice:
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 .
20 #include <sal/config.h>
21 #include <sal/log.hxx>
23 #include <com/sun/star/embed/XStorage.hpp>
24 #include <com/sun/star/embed/ElementModes.hpp>
25 #include <com/sun/star/beans/XPropertySet.hpp>
27 #include <rtl/digest.h>
28 #include <osl/file.hxx>
29 #include <sot/stg.hxx>
30 #include <sot/storinfo.hxx>
31 #include <sot/storage.hxx>
32 #include <sot/formats.hxx>
33 #include <sot/exchange.hxx>
34 #include <unotools/ucbstreamhelper.hxx>
35 #include <tools/debug.hxx>
36 #include <tools/urlobj.hxx>
37 #include <unotools/ucbhelper.hxx>
38 #include <comphelper/fileformat.h>
39 #include <com/sun/star/uno/Reference.h>
40 #include <com/sun/star/ucb/XCommandEnvironment.hpp>
41 #include <ucbhelper/content.hxx>
45 using namespace ::com::sun::star
;
47 static SvLockBytesRef
MakeLockBytes_Impl( const OUString
& rName
, StreamMode nMode
)
50 if( !rName
.isEmpty() )
52 SvStream
* pFileStm
= new SvFileStream( rName
, nMode
);
53 xLB
= new SvLockBytes( pFileStm
, true );
57 SvStream
* pCacheStm
= new SvMemoryStream();
58 xLB
= new SvLockBytes( pCacheStm
, true );
63 SotStorageStream::SotStorageStream( const OUString
& rName
, StreamMode nMode
)
64 : SvStream( MakeLockBytes_Impl( rName
, nMode
).get() )
67 if( nMode
& StreamMode::WRITE
)
73 SotStorageStream::SotStorageStream( BaseStorageStream
* pStm
)
77 if( StreamMode::WRITE
& pStm
->GetMode() )
83 SetError( pStm
->GetError() );
90 SetError( SVSTREAM_INVALID_PARAMETER
);
94 SotStorageStream::~SotStorageStream()
96 Flush(); //SetBufferSize(0);
100 void SotStorageStream::ResetError()
102 SvStream::ResetError();
104 pOwnStm
->ResetError();
107 std::size_t SotStorageStream::GetData(void* pData
, std::size_t const nSize
)
109 std::size_t nRet
= 0;
113 nRet
= pOwnStm
->Read( pData
, nSize
);
114 SetError( pOwnStm
->GetError() );
117 nRet
= SvStream::GetData( pData
, nSize
);
122 std::size_t SotStorageStream::PutData(const void* pData
, std::size_t const nSize
)
124 std::size_t nRet
= 0;
128 nRet
= pOwnStm
->Write( pData
, nSize
);
129 SetError( pOwnStm
->GetError() );
132 nRet
= SvStream::PutData( pData
, nSize
);
136 sal_uInt64
SotStorageStream::SeekPos(sal_uInt64 nPos
)
142 nRet
= pOwnStm
->Seek( nPos
);
143 SetError( pOwnStm
->GetError() );
146 nRet
= SvStream::SeekPos( nPos
);
151 void SotStorageStream::FlushData()
156 SetError( pOwnStm
->GetError() );
159 SvStream::FlushData();
162 void SotStorageStream::SetSize(sal_uInt64
const nNewSize
)
164 sal_uInt64
const nPos
= Tell();
167 pOwnStm
->SetSize( nNewSize
);
168 SetError( pOwnStm
->GetError() );
171 SvStream::SetSize( nNewSize
);
173 if( nNewSize
< nPos
)
178 sal_uInt32
SotStorageStream::GetSize() const
180 sal_uInt64 nSize
= const_cast<SotStorageStream
*>(this)->TellEnd();
184 sal_uInt64
SotStorageStream::TellEnd()
186 // Need to flush the buffer so we materialise the stream and return the correct answer
187 // otherwise we return a 0 value from StgEntry::GetSize
191 return pOwnStm
->GetSize();
193 return SvStream::TellEnd();
196 void SotStorageStream::CopyTo( SotStorageStream
* pDestStm
)
198 Flush(); // write all data
199 pDestStm
->ClearBuffer();
200 if( !pOwnStm
|| !pDestStm
->pOwnStm
)
202 // If Ole2 or not only own StorageStreams
203 sal_uInt64 nPos
= Tell(); // save position
205 pDestStm
->SetSize( 0 ); // empty target stream
207 std::unique_ptr
<sal_uInt8
[]> pMem(new sal_uInt8
[ 8192 ]);
209 while (0 != (nRead
= ReadBytes(pMem
.get(), 8192)))
211 if (nRead
!= pDestStm
->WriteBytes(pMem
.get(), nRead
))
213 SetError( SVSTREAM_GENERALERROR
);
219 pDestStm
->Seek( nPos
);
224 pOwnStm
->CopyTo( pDestStm
->pOwnStm
);
225 SetError( pOwnStm
->GetError() );
229 bool SotStorageStream::Commit()
234 if( pOwnStm
->GetError() == ERRCODE_NONE
)
236 SetError( pOwnStm
->GetError() );
238 return GetError() == ERRCODE_NONE
;
241 bool SotStorageStream::SetProperty( const OUString
& rName
, const css::uno::Any
& rValue
)
243 UCBStorageStream
* pStg
= dynamic_cast<UCBStorageStream
*>( pOwnStm
);
246 return pStg
->SetProperty( rName
, rValue
);
250 OSL_FAIL("Not implemented!");
256 * SotStorage::SotStorage()
258 * A I.. object must be passed to SvObject, because otherwise itself will
259 * create and define an IUnknown, so that all other I... objects would be
260 * destroyed with delete (Owner() == true).
261 * But IStorage objects are only used and not implemented by ourselves,
262 * therefore we pretend the IStorage object was passed from the outside
263 * and it will be freed with Release().
264 * The CreateStorage methods are needed to create an IStorage object before the
265 * call of SvObject (Own, !Own automatic).
266 * If CreateStorage has created an object, then the RefCounter was already
268 * The transfer is done in pStorageCTor and the variable is NULL, if it didn't
271 #define INIT_SotStorage() \
272 : m_pOwnStg( nullptr ) \
273 , m_pStorStm( nullptr ) \
274 , m_nError( ERRCODE_NONE ) \
275 , m_bIsRoot( false ) \
276 , m_bDelStm( false ) \
277 , m_nVersion( SOFFICE_FILEFORMAT_CURRENT )
279 #define ERASEMASK ( StreamMode::TRUNC | StreamMode::WRITE | StreamMode::SHARE_DENYALL )
281 SotStorage::SotStorage( const OUString
& rName
, StreamMode nMode
)
284 m_aName
= rName
; // save name
285 CreateStorage( true, nMode
);
286 if ( IsOLEStorage() )
287 m_nVersion
= SOFFICE_FILEFORMAT_50
;
290 void SotStorage::CreateStorage( bool bForceUCBStorage
, StreamMode nMode
)
292 DBG_ASSERT( !m_pStorStm
&& !m_pOwnStg
, "Use only in ctor!" );
293 if( !m_aName
.isEmpty() )
296 if( ( nMode
& ERASEMASK
) == ERASEMASK
)
297 ::utl::UCBContentHelper::Kill( m_aName
);
299 INetURLObject
aObj( m_aName
);
300 if ( aObj
.GetProtocol() == INetProtocol::NotValid
)
303 osl::FileBase::getFileURLFromSystemPath( m_aName
, aURL
);
305 m_aName
= aObj
.GetMainURL( INetURLObject::DecodeMechanism::NONE
);
309 m_pStorStm
= ::utl::UcbStreamHelper::CreateStream( m_aName
, nMode
).release();
310 if ( m_pStorStm
&& m_pStorStm
->GetError() )
311 DELETEZ( m_pStorStm
);
315 // try as UCBStorage, next try as OLEStorage
316 bool bIsUCBStorage
= UCBStorage::IsStorageFile( m_pStorStm
);
317 if ( !bIsUCBStorage
&& bForceUCBStorage
)
318 // if UCBStorage has priority, it should not be used only if it is really an OLEStorage
319 bIsUCBStorage
= !Storage::IsStorageFile( m_pStorStm
);
323 if ( !(UCBStorage::GetLinkedFile( *m_pStorStm
).isEmpty()) )
325 // detect special unpacked storages
326 m_pOwnStg
= new UCBStorage( *m_pStorStm
, true );
331 // UCBStorage always works directly on the UCB content, so discard the stream first
332 DELETEZ( m_pStorStm
);
333 m_pOwnStg
= new UCBStorage( m_aName
, nMode
, true, true/*bIsRoot*/ );
338 // OLEStorage can be opened with a stream
339 m_pOwnStg
= new Storage( *m_pStorStm
, true );
343 else if ( bForceUCBStorage
)
345 m_pOwnStg
= new UCBStorage( m_aName
, nMode
, true, true/*bIsRoot*/ );
346 SetError( ERRCODE_IO_NOTSUPPORTED
);
350 m_pOwnStg
= new Storage( m_aName
, nMode
, true );
351 SetError( ERRCODE_IO_NOTSUPPORTED
);
357 if ( bForceUCBStorage
)
358 m_pOwnStg
= new UCBStorage( m_aName
, nMode
, true, true/*bIsRoot*/ );
360 m_pOwnStg
= new Storage( m_aName
, nMode
, true );
361 m_aName
= m_pOwnStg
->GetName();
364 SetError( m_pOwnStg
->GetError() );
366 SignAsRoot( m_pOwnStg
->IsRoot() );
369 SotStorage::SotStorage( bool bUCBStorage
, const OUString
& rName
, StreamMode nMode
)
373 CreateStorage( bUCBStorage
, nMode
);
374 if ( IsOLEStorage() )
375 m_nVersion
= SOFFICE_FILEFORMAT_50
;
378 SotStorage::SotStorage( BaseStorage
* pStor
)
383 m_aName
= pStor
->GetName(); // save name
384 SignAsRoot( pStor
->IsRoot() );
385 SetError( pStor
->GetError() );
389 const ErrCode nErr
= m_pOwnStg
? m_pOwnStg
->GetError() : SVSTREAM_CANNOT_MAKE
;
391 if ( IsOLEStorage() )
392 m_nVersion
= SOFFICE_FILEFORMAT_50
;
395 SotStorage::SotStorage( bool bUCBStorage
, SvStream
& rStm
)
398 SetError( rStm
.GetError() );
400 // try as UCBStorage, next try as OLEStorage
401 if ( UCBStorage::IsStorageFile( &rStm
) || bUCBStorage
)
402 m_pOwnStg
= new UCBStorage( rStm
, false );
404 m_pOwnStg
= new Storage( rStm
, false );
406 SetError( m_pOwnStg
->GetError() );
408 if ( IsOLEStorage() )
409 m_nVersion
= SOFFICE_FILEFORMAT_50
;
411 SignAsRoot( m_pOwnStg
->IsRoot() );
414 SotStorage::SotStorage( SvStream
& rStm
)
417 SetError( rStm
.GetError() );
419 // try as UCBStorage, next try as OLEStorage
420 if ( UCBStorage::IsStorageFile( &rStm
) )
421 m_pOwnStg
= new UCBStorage( rStm
, false );
423 m_pOwnStg
= new Storage( rStm
, false );
425 SetError( m_pOwnStg
->GetError() );
427 if ( IsOLEStorage() )
428 m_nVersion
= SOFFICE_FILEFORMAT_50
;
430 SignAsRoot( m_pOwnStg
->IsRoot() );
433 SotStorage::SotStorage( SvStream
* pStm
, bool bDelete
)
436 SetError( pStm
->GetError() );
438 // try as UCBStorage, next try as OLEStorage
439 if ( UCBStorage::IsStorageFile( pStm
) )
440 m_pOwnStg
= new UCBStorage( *pStm
, false );
442 m_pOwnStg
= new Storage( *pStm
, false );
444 SetError( m_pOwnStg
->GetError() );
448 if ( IsOLEStorage() )
449 m_nVersion
= SOFFICE_FILEFORMAT_50
;
451 SignAsRoot( m_pOwnStg
->IsRoot() );
454 SotStorage::~SotStorage()
461 std::unique_ptr
<SvMemoryStream
> SotStorage::CreateMemoryStream()
463 std::unique_ptr
<SvMemoryStream
> pStm(new SvMemoryStream( 0x8000, 0x8000 ));
464 tools::SvRef
<SotStorage
> aStg
= new SotStorage( *pStm
);
465 if( CopyTo( aStg
.get() ) )
471 aStg
.clear(); // release storage beforehand
477 bool SotStorage::IsStorageFile( const OUString
& rFileName
)
479 OUString
aName( rFileName
);
480 INetURLObject
aObj( aName
);
481 if ( aObj
.GetProtocol() == INetProtocol::NotValid
)
484 osl::FileBase::getFileURLFromSystemPath( aName
, aURL
);
486 aName
= aObj
.GetMainURL( INetURLObject::DecodeMechanism::NONE
);
489 std::unique_ptr
<SvStream
> pStm(::utl::UcbStreamHelper::CreateStream( aName
, StreamMode::STD_READ
));
490 bool bRet
= SotStorage::IsStorageFile( pStm
.get() );
494 bool SotStorage::IsStorageFile( SvStream
* pStream
)
496 /** code for new storages must come first! **/
499 sal_uInt64 nPos
= pStream
->Tell();
500 bool bRet
= UCBStorage::IsStorageFile( pStream
);
502 bRet
= Storage::IsStorageFile( pStream
);
503 pStream
->Seek( nPos
);
510 const OUString
& SotStorage::GetName() const
512 if( m_aName
.isEmpty() && m_pOwnStg
)
513 const_cast<SotStorage
*>(this)->m_aName
= m_pOwnStg
->GetName();
517 void SotStorage::SetClass( const SvGlobalName
& rName
,
518 SotClipboardFormatId nOriginalClipFormat
,
519 const OUString
& rUserTypeName
)
522 m_pOwnStg
->SetClass( rName
, nOriginalClipFormat
, rUserTypeName
);
524 SetError( SVSTREAM_GENERALERROR
);
527 SvGlobalName
SotStorage::GetClassName()
531 aGN
= m_pOwnStg
->GetClassName();
533 SetError( SVSTREAM_GENERALERROR
);
537 SotClipboardFormatId
SotStorage::GetFormat()
539 SotClipboardFormatId nFormat
= SotClipboardFormatId::NONE
;
541 nFormat
= m_pOwnStg
->GetFormat();
543 SetError( SVSTREAM_GENERALERROR
);
547 OUString
SotStorage::GetUserName()
551 aName
= m_pOwnStg
->GetUserName();
553 SetError( SVSTREAM_GENERALERROR
);
557 void SotStorage::FillInfoList( SvStorageInfoList
* pFillList
) const
560 m_pOwnStg
->FillInfoList( pFillList
);
563 bool SotStorage::CopyTo( SotStorage
* pDestStg
)
565 if( m_pOwnStg
&& pDestStg
->m_pOwnStg
)
567 m_pOwnStg
->CopyTo( pDestStg
->m_pOwnStg
);
568 SetError( m_pOwnStg
->GetError() );
569 pDestStg
->m_aKey
= m_aKey
;
570 pDestStg
->m_nVersion
= m_nVersion
;
573 SetError( SVSTREAM_GENERALERROR
);
575 return ERRCODE_NONE
== GetError();
578 bool SotStorage::Commit()
582 if( !m_pOwnStg
->Commit() )
583 SetError( m_pOwnStg
->GetError() );
586 SetError( SVSTREAM_GENERALERROR
);
588 return ERRCODE_NONE
== GetError();
591 SotStorageStream
* SotStorage::OpenSotStream( const OUString
& rEleName
,
594 SotStorageStream
* pStm
= nullptr;
597 // enable full Ole patches,
598 // regardless what is coming, only exclusively allowed
599 nMode
|= StreamMode::SHARE_DENYALL
;
600 ErrCode nE
= m_pOwnStg
->GetError();
601 BaseStorageStream
* p
= m_pOwnStg
->OpenStream( rEleName
, nMode
);
602 pStm
= new SotStorageStream( p
);
605 m_pOwnStg
->ResetError(); // don't set error
606 if( nMode
& StreamMode::TRUNC
)
610 SetError( SVSTREAM_GENERALERROR
);
615 SotStorage
* SotStorage::OpenSotStorage( const OUString
& rEleName
,
621 nMode
|= StreamMode::SHARE_DENYALL
;
622 ErrCode nE
= m_pOwnStg
->GetError();
623 BaseStorage
* p
= m_pOwnStg
->OpenStorage(rEleName
, nMode
, !transacted
);
626 SotStorage
* pStor
= new SotStorage( p
);
628 m_pOwnStg
->ResetError(); // don't set error
634 SetError( SVSTREAM_GENERALERROR
);
639 bool SotStorage::IsStorage( const OUString
& rEleName
) const
641 // a little bit faster
643 return m_pOwnStg
->IsStorage( rEleName
);
648 bool SotStorage::IsStream( const OUString
& rEleName
) const
650 // a little bit faster
652 return m_pOwnStg
->IsStream( rEleName
);
657 bool SotStorage::IsContained( const OUString
& rEleName
) const
659 // a little bit faster
661 return m_pOwnStg
->IsContained( rEleName
);
666 bool SotStorage::Remove( const OUString
& rEleName
)
670 m_pOwnStg
->Remove( rEleName
);
671 SetError( m_pOwnStg
->GetError() );
674 SetError( SVSTREAM_GENERALERROR
);
676 return ERRCODE_NONE
== GetError();
679 bool SotStorage::CopyTo( const OUString
& rEleName
,
680 SotStorage
* pNewSt
, const OUString
& rNewName
)
684 m_pOwnStg
->CopyTo( rEleName
, pNewSt
->m_pOwnStg
, rNewName
);
685 SetError( m_pOwnStg
->GetError() );
686 SetError( pNewSt
->GetError() );
689 SetError( SVSTREAM_GENERALERROR
);
691 return ERRCODE_NONE
== GetError();
694 bool SotStorage::Validate()
696 DBG_ASSERT( m_bIsRoot
, "Validate only if root storage" );
698 return m_pOwnStg
->ValidateFAT();
703 bool SotStorage::IsOLEStorage() const
705 UCBStorage
* pStg
= dynamic_cast<UCBStorage
*>( m_pOwnStg
);
709 bool SotStorage::IsOLEStorage( const OUString
& rFileName
)
711 return Storage::IsStorageFile( rFileName
);
714 bool SotStorage::IsOLEStorage( SvStream
* pStream
)
716 return Storage::IsStorageFile( pStream
);
719 SotStorage
* SotStorage::OpenOLEStorage( const css::uno::Reference
< css::embed::XStorage
>& xStorage
,
720 const OUString
& rEleName
, StreamMode nMode
)
722 sal_Int32 nEleMode
= embed::ElementModes::SEEKABLEREAD
;
723 if ( nMode
& StreamMode::WRITE
)
724 nEleMode
|= embed::ElementModes::WRITE
;
725 if ( nMode
& StreamMode::TRUNC
)
726 nEleMode
|= embed::ElementModes::TRUNCATE
;
727 if ( nMode
& StreamMode::NOCREATE
)
728 nEleMode
|= embed::ElementModes::NOCREATE
;
730 std::unique_ptr
<SvStream
> pStream
;
733 uno::Reference
< io::XStream
> xStream
= xStorage
->openStreamElement( rEleName
, nEleMode
);
735 // TODO/LATER: should it be done this way?
736 if ( nMode
& StreamMode::WRITE
)
738 uno::Reference
< beans::XPropertySet
> xStreamProps( xStream
, uno::UNO_QUERY_THROW
);
739 xStreamProps
->setPropertyValue( "MediaType",
740 uno::makeAny( OUString( "application/vnd.sun.star.oleobject" ) ) );
743 pStream
= utl::UcbStreamHelper::CreateStream( xStream
);
745 catch ( uno::Exception
& )
747 //TODO/LATER: ErrorHandling
748 pStream
.reset( new SvMemoryStream
);
749 pStream
->SetError( ERRCODE_IO_GENERAL
);
752 return new SotStorage( pStream
.release(), true );
755 SotClipboardFormatId
SotStorage::GetFormatID( const css::uno::Reference
< css::embed::XStorage
>& xStorage
)
757 uno::Reference
< beans::XPropertySet
> xProps( xStorage
, uno::UNO_QUERY
);
759 return SotClipboardFormatId::NONE
;
764 xProps
->getPropertyValue("MediaType") >>= aMediaType
;
766 catch (uno::Exception
const& e
)
768 SAL_INFO("sot", "SotStorage::GetFormatID: exception: " << e
);
771 if ( !aMediaType
.isEmpty() )
773 css::datatransfer::DataFlavor aDataFlavor
;
774 aDataFlavor
.MimeType
= aMediaType
;
775 return SotExchange::GetFormat( aDataFlavor
);
778 return SotClipboardFormatId::NONE
;
781 sal_Int32
SotStorage::GetVersion( const css::uno::Reference
< css::embed::XStorage
>& xStorage
)
783 SotClipboardFormatId nSotFormatID
= SotStorage::GetFormatID( xStorage
);
784 switch( nSotFormatID
)
786 case SotClipboardFormatId::STARWRITER_8
:
787 case SotClipboardFormatId::STARWRITER_8_TEMPLATE
:
788 case SotClipboardFormatId::STARWRITERWEB_8
:
789 case SotClipboardFormatId::STARWRITERGLOB_8
:
790 case SotClipboardFormatId::STARWRITERGLOB_8_TEMPLATE
:
791 case SotClipboardFormatId::STARDRAW_8
:
792 case SotClipboardFormatId::STARDRAW_8_TEMPLATE
:
793 case SotClipboardFormatId::STARIMPRESS_8
:
794 case SotClipboardFormatId::STARIMPRESS_8_TEMPLATE
:
795 case SotClipboardFormatId::STARCALC_8
:
796 case SotClipboardFormatId::STARCALC_8_TEMPLATE
:
797 case SotClipboardFormatId::STARCHART_8
:
798 case SotClipboardFormatId::STARCHART_8_TEMPLATE
:
799 case SotClipboardFormatId::STARMATH_8
:
800 case SotClipboardFormatId::STARMATH_8_TEMPLATE
:
801 return SOFFICE_FILEFORMAT_8
;
802 case SotClipboardFormatId::STARWRITER_60
:
803 case SotClipboardFormatId::STARWRITERWEB_60
:
804 case SotClipboardFormatId::STARWRITERGLOB_60
:
805 case SotClipboardFormatId::STARDRAW_60
:
806 case SotClipboardFormatId::STARIMPRESS_60
:
807 case SotClipboardFormatId::STARCALC_60
:
808 case SotClipboardFormatId::STARCHART_60
:
809 case SotClipboardFormatId::STARMATH_60
:
810 return SOFFICE_FILEFORMAT_60
;
819 void traverse(const tools::SvRef
<SotStorage
>& rStorage
, std::vector
<unsigned char>& rBuf
)
821 SvStorageInfoList infos
;
823 rStorage
->FillInfoList(&infos
);
825 for (const auto& info
: infos
)
829 // try to open and read all content
830 tools::SvRef
<SotStorageStream
> xStream(rStorage
->OpenSotStream(info
.GetName(), StreamMode::STD_READ
));
831 const size_t nSize
= xStream
->GetSize();
832 const size_t nRead
= xStream
->ReadBytes(rBuf
.data(), nSize
);
833 SAL_INFO("sot", "Read " << nRead
<< "bytes");
835 else if (info
.IsStorage())
837 tools::SvRef
<SotStorage
> xStorage(rStorage
->OpenSotStorage(info
.GetName(), StreamMode::STD_READ
));
839 // continue with children
840 traverse(xStorage
, rBuf
);
846 extern "C" SAL_DLLPUBLIC_EXPORT
bool TestImportOLE2(SvStream
&rStream
)
850 size_t nSize
= rStream
.remainingSize();
851 tools::SvRef
<SotStorage
> xRootStorage(new SotStorage(&rStream
, false));
852 std::vector
<unsigned char> aTmpBuf(nSize
);
853 traverse(xRootStorage
, aTmpBuf
);
862 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */