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 <osl/diagnose.h>
21 #include <o3tl/char16_t2wchar_t.hxx>
23 #include "XTDataObject.hxx"
24 #include <com/sun/star/datatransfer/DataFlavor.hpp>
25 #include "../misc/ImplHelper.hxx"
26 #include "DTransHelper.hxx"
27 #include "TxtCnvtHlp.hxx"
28 #include <com/sun/star/datatransfer/UnsupportedFlavorException.hpp>
29 #include <com/sun/star/datatransfer/clipboard/XClipboardEx.hpp>
30 #include <com/sun/star/awt/AsyncCallback.hpp>
31 #include <com/sun/star/awt/XCallback.hpp>
32 #include "FmtFilter.hxx"
33 #include <cppuhelper/implbase.hxx>
35 #if !defined WIN32_LEAN_AND_MEAN
36 # define WIN32_LEAN_AND_MEAN
41 using namespace com::sun::star::datatransfer
;
42 using namespace com::sun::star::datatransfer::clipboard
;
43 using namespace com::sun::star::uno
;
44 using namespace com::sun::star::lang
;
48 void setupStgMedium( const FORMATETC
& fetc
,
49 CStgTransferHelper
& stgTransHlp
,
50 STGMEDIUM
& stgmedium
)
52 stgmedium
.pUnkForRelease
= nullptr;
54 if ( fetc
.cfFormat
== CF_METAFILEPICT
)
56 stgmedium
.tymed
= TYMED_MFPICT
;
57 stgmedium
.hMetaFilePict
= static_cast< HMETAFILEPICT
>( stgTransHlp
.getHGlobal( ) );
59 else if ( fetc
.cfFormat
== CF_ENHMETAFILE
)
61 stgmedium
.tymed
= TYMED_ENHMF
;
62 stgmedium
.hEnhMetaFile
= static_cast< HENHMETAFILE
>( stgTransHlp
.getHGlobal( ) );
64 else if ( fetc
.tymed
& TYMED_HGLOBAL
)
66 stgmedium
.tymed
= TYMED_HGLOBAL
;
67 stgmedium
.hGlobal
= stgTransHlp
.getHGlobal( );
69 else if ( fetc
.tymed
& TYMED_ISTREAM
)
71 stgmedium
.tymed
= TYMED_ISTREAM
;
72 stgTransHlp
.getIStream( &stgmedium
.pstm
);
79 We need to destroy XTransferable in the main thread to avoid dead lock
80 when locking in the clipboard thread. So we transfer the ownership of the
81 XTransferable reference to this object and release it when the callback
82 is executed in main thread.
84 class AsyncDereference
: public cppu::WeakImplHelper
<css::awt::XCallback
>
86 Reference
<XTransferable
> maTransferable
;
89 AsyncDereference(css::uno::Reference
<css::datatransfer::XTransferable
> const & rTransferable
)
90 : maTransferable(rTransferable
)
93 virtual void SAL_CALL
notify(css::uno::Any
const &) override
95 maTransferable
.set(nullptr);
99 // a helper class that will be thrown by the function validateFormatEtc
101 class CInvalidFormatEtcException
105 explicit CInvalidFormatEtcException( HRESULT hr
) : m_hr( hr
) {};
108 void validateFormatEtc( LPFORMATETC lpFormatEtc
)
110 OSL_ASSERT( lpFormatEtc
);
112 if ( lpFormatEtc
->lindex
!= -1 )
113 throw CInvalidFormatEtcException( DV_E_LINDEX
);
115 if ( !(lpFormatEtc
->dwAspect
& DVASPECT_CONTENT
) &&
116 !(lpFormatEtc
->dwAspect
& DVASPECT_SHORTNAME
) )
117 throw CInvalidFormatEtcException( DV_E_DVASPECT
);
119 if ( !(lpFormatEtc
->tymed
& TYMED_HGLOBAL
) &&
120 !(lpFormatEtc
->tymed
& TYMED_ISTREAM
) &&
121 !(lpFormatEtc
->tymed
& TYMED_MFPICT
) &&
122 !(lpFormatEtc
->tymed
& TYMED_ENHMF
) )
123 throw CInvalidFormatEtcException( DV_E_TYMED
);
125 if ( lpFormatEtc
->cfFormat
== CF_METAFILEPICT
&&
126 !(lpFormatEtc
->tymed
& TYMED_MFPICT
) )
127 throw CInvalidFormatEtcException( DV_E_TYMED
);
129 if ( lpFormatEtc
->cfFormat
== CF_ENHMETAFILE
&&
130 !(lpFormatEtc
->tymed
& TYMED_ENHMF
) )
131 throw CInvalidFormatEtcException( DV_E_TYMED
);
134 void invalidateStgMedium( STGMEDIUM
& stgmedium
)
136 stgmedium
.tymed
= TYMED_NULL
;
139 HRESULT
translateStgExceptionCode( HRESULT hr
)
145 case STG_E_MEDIUMFULL
:
150 hrTransl
= E_UNEXPECTED
;
158 void renderDataAndSetupStgMedium(
159 const sal_Int8
* lpStorage
, const FORMATETC
& fetc
, sal_uInt32 nInitStgSize
,
160 sal_uInt32 nBytesToTransfer
, STGMEDIUM
& stgmedium
)
162 OSL_PRECOND( !nInitStgSize
|| (nInitStgSize
>= nBytesToTransfer
),
163 "Memory size less than number of bytes to transfer" );
165 CStgTransferHelper
stgTransfHelper( AUTO_INIT
);
167 // setup storage size
168 if ( nInitStgSize
> 0 )
169 stgTransfHelper
.init( nInitStgSize
);
171 #if OSL_DEBUG_LEVEL > 0
172 sal_uInt32 nBytesWritten
= 0;
173 stgTransfHelper
.write( lpStorage
, nBytesToTransfer
, &nBytesWritten
);
174 OSL_ASSERT( nBytesWritten
== nBytesToTransfer
);
176 stgTransfHelper
.write( lpStorage
, nBytesToTransfer
);
179 setupStgMedium( fetc
, stgTransfHelper
, stgmedium
);
184 CXTDataObject::CXTDataObject( const Reference
< XComponentContext
>& rxContext
,
185 const Reference
< XTransferable
>& aXTransferable
)
187 , m_XTransferable( aXTransferable
)
188 , m_XComponentContext( rxContext
)
189 , m_bFormatEtcContainerInitialized( false )
190 , m_DataFormatTranslator( rxContext
)
191 , m_FormatRegistrar( rxContext
, m_DataFormatTranslator
)
195 CXTDataObject::~CXTDataObject()
197 css::awt::AsyncCallback::create(m_XComponentContext
)->addCallback(
198 new AsyncDereference(m_XTransferable
),
202 // IUnknown->QueryInterface
204 STDMETHODIMP
CXTDataObject::QueryInterface( REFIID iid
, void** ppvObject
)
206 if ( nullptr == ppvObject
)
209 HRESULT hr
= E_NOINTERFACE
;
211 *ppvObject
= nullptr;
212 if ( ( __uuidof( IUnknown
) == iid
) ||
213 ( __uuidof( IDataObject
) == iid
) )
215 *ppvObject
= static_cast< IUnknown
* >( this );
216 static_cast<LPUNKNOWN
>(*ppvObject
)->AddRef( );
225 STDMETHODIMP_(ULONG
) CXTDataObject::AddRef( )
227 return static_cast< ULONG
>( InterlockedIncrement( &m_nRefCnt
) );
232 STDMETHODIMP_(ULONG
) CXTDataObject::Release( )
235 static_cast< ULONG
>( InterlockedDecrement( &m_nRefCnt
) );
243 STDMETHODIMP
CXTDataObject::GetData( FORMATETC
* pFormatetc
, STGMEDIUM
* pmedium
)
245 if ( !(pFormatetc
&& pmedium
) )
250 // prepare data transfer
251 invalidateStgMedium( *pmedium
);
252 validateFormatEtc( pFormatetc
);
254 // handle locale request, because locale is an artificial format for us
255 if ( CF_LOCALE
== pFormatetc
->cfFormat
)
256 renderLocaleAndSetupStgMedium( *pFormatetc
, *pmedium
);
257 else if ( CF_UNICODETEXT
== pFormatetc
->cfFormat
)
258 renderUnicodeAndSetupStgMedium( *pFormatetc
, *pmedium
);
260 renderAnyDataAndSetupStgMedium( *pFormatetc
, *pmedium
);
262 catch(UnsupportedFlavorException
&)
264 HRESULT hr
= DV_E_FORMATETC
;
266 CFormatEtc
aFormatetc(*pFormatetc
);
267 if (CFormatRegistrar::isSynthesizeableFormat(aFormatetc
))
268 hr
= renderSynthesizedFormatAndSetupStgMedium( *pFormatetc
, *pmedium
);
272 catch( CInvalidFormatEtcException
& ex
)
276 catch( CStgTransferHelper::CStgTransferException
& ex
)
278 return translateStgExceptionCode( ex
.m_hr
);
289 void CXTDataObject::renderLocaleAndSetupStgMedium(
290 FORMATETC
const & fetc
, STGMEDIUM
& stgmedium
)
292 if ( !m_FormatRegistrar
.hasSynthesizedLocale( ) )
293 throw CInvalidFormatEtcException( DV_E_FORMATETC
);
294 LCID lcid
= CFormatRegistrar::getSynthesizedLocale( );
295 renderDataAndSetupStgMedium(
296 reinterpret_cast< sal_Int8
* >( &lcid
),
303 void CXTDataObject::renderUnicodeAndSetupStgMedium(
304 FORMATETC
const & fetc
, STGMEDIUM
& stgmedium
)
306 DataFlavor aFlavor
= formatEtcToDataFlavor( fetc
);
308 Any aAny
= m_XTransferable
->getTransferData( aFlavor
);
310 // unfortunately not all transferables fulfill the
311 // spec. and do throw an UnsupportedFlavorException
312 // so we must check the any
313 if ( !aAny
.hasValue( ) )
315 OSL_FAIL( "XTransferable should throw an exception if ask for an unsupported flavor" );
316 throw UnsupportedFlavorException( );
322 sal_uInt32 nBytesToTransfer
= aText
.getLength( ) * sizeof( sal_Unicode
);
324 // to be sure there is an ending 0
325 sal_uInt32 nRequiredMemSize
= nBytesToTransfer
+ sizeof( sal_Unicode
);
327 renderDataAndSetupStgMedium(
328 reinterpret_cast< const sal_Int8
* >( aText
.getStr( ) ),
335 void CXTDataObject::renderAnyDataAndSetupStgMedium(
336 FORMATETC
& fetc
, STGMEDIUM
& stgmedium
)
338 DataFlavor aFlavor
= formatEtcToDataFlavor( fetc
);
340 Any aAny
= m_XTransferable
->getTransferData( aFlavor
);
342 // unfortunately not all transferables fulfill the
343 // spec. and do throw an UnsupportedFlavorException
344 // so we must check the any
345 if ( !aAny
.hasValue( ) )
347 OSL_FAIL( "XTransferable should throw an exception if ask for an unsupported flavor" );
348 throw UnsupportedFlavorException( );
351 // unfortunately not all transferables fulfill the
352 // spec. and do throw an UnsupportedFlavorException
353 // so we must check the any
354 if ( !aAny
.hasValue( ) )
355 throw UnsupportedFlavorException( );
357 Sequence
< sal_Int8
> clipDataStream
;
358 aAny
>>= clipDataStream
;
360 sal_uInt32 nRequiredMemSize
= 0;
361 if ( CDataFormatTranslator::isOemOrAnsiTextFormat( fetc
.cfFormat
) )
362 nRequiredMemSize
= sizeof( sal_Int8
) * clipDataStream
.getLength( ) + 1;
364 // prepare data for transmission
365 // #i124085# DIBV5 should not happen for now, but keep as hint here
366 if ( CF_DIBV5
== fetc
.cfFormat
|| CF_DIB
== fetc
.cfFormat
)
369 if(CF_DIBV5
== fetc
.cfFormat
)
371 OSL_ENSURE(sal_uInt32(clipDataStream
.getLength()) > (sizeof(BITMAPFILEHEADER
) + sizeof(BITMAPV5HEADER
)), "Wrong size on CF_DIBV5 data (!)");
373 else // CF_DIB == fetc.cfFormat
375 OSL_ENSURE(sal_uInt32(clipDataStream
.getLength()) > (sizeof(BITMAPFILEHEADER
) + sizeof(BITMAPINFOHEADER
)), "Wrong size on CF_DIB data (!)");
379 // remove BITMAPFILEHEADER
380 clipDataStream
= OOBmpToWinDIB( clipDataStream
);
383 if ( CF_METAFILEPICT
== fetc
.cfFormat
)
385 stgmedium
.tymed
= TYMED_MFPICT
;
386 stgmedium
.hMetaFilePict
= OOMFPictToWinMFPict( clipDataStream
);
387 stgmedium
.pUnkForRelease
= nullptr;
389 else if( CF_ENHMETAFILE
== fetc
.cfFormat
)
391 stgmedium
.tymed
= TYMED_ENHMF
;
392 stgmedium
.hMetaFilePict
= OOMFPictToWinENHMFPict( clipDataStream
);
393 stgmedium
.pUnkForRelease
= nullptr;
396 renderDataAndSetupStgMedium(
397 clipDataStream
.getArray( ),
400 clipDataStream
.getLength( ),
404 HRESULT
CXTDataObject::renderSynthesizedFormatAndSetupStgMedium( FORMATETC
& fetc
, STGMEDIUM
& stgmedium
)
410 if ( CF_UNICODETEXT
== fetc
.cfFormat
)
411 // the transferable seems to have only text
412 renderSynthesizedUnicodeAndSetupStgMedium( fetc
, stgmedium
);
413 else if ( CDataFormatTranslator::isOemOrAnsiTextFormat( fetc
.cfFormat
) )
414 // the transferable seems to have only unicode text
415 renderSynthesizedTextAndSetupStgMedium( fetc
, stgmedium
);
417 // the transferable seems to have only text/html
418 renderSynthesizedHtmlAndSetupStgMedium( fetc
, stgmedium
);
420 catch(UnsupportedFlavorException
&)
424 catch( CInvalidFormatEtcException
& )
426 OSL_FAIL( "Unexpected exception" );
428 catch( CStgTransferHelper::CStgTransferException
& ex
)
430 return translateStgExceptionCode( ex
.m_hr
);
440 // the transferable must have only text, so we will synthesize unicode text
442 void CXTDataObject::renderSynthesizedUnicodeAndSetupStgMedium( FORMATETC
const & fetc
, STGMEDIUM
& stgmedium
)
444 OSL_ASSERT( CF_UNICODETEXT
== fetc
.cfFormat
);
446 Any aAny
= m_XTransferable
->getTransferData( m_FormatRegistrar
.getRegisteredTextFlavor( ) );
448 // unfortunately not all transferables fulfill the
449 // spec. and do throw an UnsupportedFlavorException
450 // so we must check the any
451 if ( !aAny
.hasValue( ) )
453 OSL_FAIL( "XTransferable should throw an exception if ask for an unsupported flavor" );
454 throw UnsupportedFlavorException( );
457 Sequence
< sal_Int8
> aText
;
460 CStgTransferHelper stgTransfHelper
;
462 MultiByteToWideCharEx(
463 CFormatRegistrar::getRegisteredTextCodePage( ),
464 reinterpret_cast< char* >( aText
.getArray( ) ),
468 setupStgMedium( fetc
, stgTransfHelper
, stgmedium
);
471 // the transferable must have only unicode text so we will synthesize text
473 void CXTDataObject::renderSynthesizedTextAndSetupStgMedium( FORMATETC
& fetc
, STGMEDIUM
& stgmedium
)
475 OSL_ASSERT( CDataFormatTranslator::isOemOrAnsiTextFormat( fetc
.cfFormat
) );
477 DataFlavor aFlavor
= formatEtcToDataFlavor(
478 CDataFormatTranslator::getFormatEtcForClipformat( CF_UNICODETEXT
) );
480 Any aAny
= m_XTransferable
->getTransferData( aFlavor
);
482 // unfortunately not all transferables fulfill the
483 // spec. and do throw an UnsupportedFlavorException
484 // so we must check the any
485 if ( !aAny
.hasValue( ) )
487 OSL_FAIL( "XTransferable should throw an exception if ask for an unsupported flavor" );
488 throw UnsupportedFlavorException( );
491 OUString aUnicodeText
;
492 aAny
>>= aUnicodeText
;
494 CStgTransferHelper stgTransfHelper
;
496 WideCharToMultiByteEx(
498 o3tl::toW( aUnicodeText
.getStr( ) ),
499 aUnicodeText
.getLength( ),
502 setupStgMedium( fetc
, stgTransfHelper
, stgmedium
);
505 void CXTDataObject::renderSynthesizedHtmlAndSetupStgMedium( FORMATETC
& fetc
, STGMEDIUM
& stgmedium
)
507 OSL_ASSERT( CDataFormatTranslator::isHTMLFormat( fetc
.cfFormat
) );
511 // creating a DataFlavor on the fly
512 aFlavor
.MimeType
= "text/html";
513 aFlavor
.DataType
= cppu::UnoType
<Sequence
< sal_Int8
>>::get();
515 Any aAny
= m_XTransferable
->getTransferData( aFlavor
);
517 // unfortunately not all transferables fulfill the
518 // spec. and do throw an UnsupportedFlavorException
519 // so we must check the any
520 if ( !aAny
.hasValue( ) )
522 OSL_FAIL( "XTransferable should throw an exception if ask for an unsupported flavor" );
523 throw UnsupportedFlavorException( );
526 Sequence
< sal_Int8
> aTextHtmlSequence
;
527 aAny
>>= aTextHtmlSequence
;
529 Sequence
< sal_Int8
> aHTMLFormatSequence
= TextHtmlToHTMLFormat( aTextHtmlSequence
);
531 sal_uInt32 nBytesToTransfer
= aHTMLFormatSequence
.getLength( );
533 renderDataAndSetupStgMedium(
534 reinterpret_cast< const sal_Int8
* >( aHTMLFormatSequence
.getArray( ) ),
541 // IDataObject->EnumFormatEtc
543 STDMETHODIMP
CXTDataObject::EnumFormatEtc(
544 DWORD dwDirection
, IEnumFORMATETC
** ppenumFormatetc
)
546 if ( nullptr == ppenumFormatetc
)
549 if ( DATADIR_SET
== dwDirection
)
552 *ppenumFormatetc
= nullptr;
554 InitializeFormatEtcContainer( );
557 if ( DATADIR_GET
== dwDirection
)
559 *ppenumFormatetc
= new CEnumFormatEtc( this, m_FormatEtcContainer
);
560 static_cast< LPUNKNOWN
>( *ppenumFormatetc
)->AddRef( );
570 // IDataObject->QueryGetData
572 STDMETHODIMP
CXTDataObject::QueryGetData( FORMATETC
* pFormatetc
)
574 if ( (nullptr == pFormatetc
) || IsBadReadPtr( pFormatetc
, sizeof( FORMATETC
) ) )
577 InitializeFormatEtcContainer( );
579 CFormatEtc
aFormatetc(*pFormatetc
);
580 return m_FormatEtcContainer
.hasFormatEtc(aFormatetc
) ? S_OK
: S_FALSE
;
583 // IDataObject->GetDataHere
585 STDMETHODIMP
CXTDataObject::GetDataHere( FORMATETC
*, STGMEDIUM
* )
590 // IDataObject->GetCanonicalFormatEtc
592 STDMETHODIMP
CXTDataObject::GetCanonicalFormatEtc( FORMATETC
*, FORMATETC
* )
597 // IDataObject->SetData
599 STDMETHODIMP
CXTDataObject::SetData( FORMATETC
*, STGMEDIUM
*, BOOL
)
604 // IDataObject->DAdvise
606 STDMETHODIMP
CXTDataObject::DAdvise( FORMATETC
*, DWORD
, IAdviseSink
*, DWORD
* )
611 // IDataObject->DUnadvise
613 STDMETHODIMP
CXTDataObject::DUnadvise( DWORD
)
618 // IDataObject->EnumDAdvise
620 STDMETHODIMP
CXTDataObject::EnumDAdvise( IEnumSTATDATA
** )
625 // for our convenience
627 CXTDataObject::operator IDataObject
*( )
629 return static_cast< IDataObject
* >( this );
633 DataFlavor
CXTDataObject::formatEtcToDataFlavor( const FORMATETC
& aFormatEtc
) const
637 if ( m_FormatRegistrar
.hasSynthesizedLocale( ) )
639 m_DataFormatTranslator
.getDataFlavorFromFormatEtc( aFormatEtc
, CFormatRegistrar::getSynthesizedLocale( ) );
641 aFlavor
= m_DataFormatTranslator
.getDataFlavorFromFormatEtc( aFormatEtc
);
643 if ( !aFlavor
.MimeType
.getLength( ) )
644 throw UnsupportedFlavorException( );
649 inline void CXTDataObject::InitializeFormatEtcContainer( )
651 if ( !m_bFormatEtcContainerInitialized
)
653 m_FormatRegistrar
.RegisterFormats( m_XTransferable
, m_FormatEtcContainer
);
654 m_bFormatEtcContainerInitialized
= true;
658 CEnumFormatEtc::CEnumFormatEtc( LPUNKNOWN lpUnkOuter
, const CFormatEtcContainer
& aFormatEtcContainer
) :
660 m_lpUnkOuter( lpUnkOuter
),
661 m_FormatEtcContainer( aFormatEtcContainer
)
666 // IUnknown->QueryInterface
668 STDMETHODIMP
CEnumFormatEtc::QueryInterface( REFIID iid
, void** ppvObject
)
670 if ( nullptr == ppvObject
)
673 HRESULT hr
= E_NOINTERFACE
;
675 *ppvObject
= nullptr;
677 if ( ( __uuidof( IUnknown
) == iid
) ||
678 ( __uuidof( IEnumFORMATETC
) == iid
) )
680 *ppvObject
= static_cast< IUnknown
* >( this );
681 static_cast< LPUNKNOWN
>( *ppvObject
)->AddRef( );
690 STDMETHODIMP_(ULONG
) CEnumFormatEtc::AddRef( )
692 // keep the dataobject alive
693 m_lpUnkOuter
->AddRef( );
694 return InterlockedIncrement( &m_nRefCnt
);
699 STDMETHODIMP_(ULONG
) CEnumFormatEtc::Release( )
701 // release the outer dataobject
702 m_lpUnkOuter
->Release( );
704 ULONG nRefCnt
= InterlockedDecrement( &m_nRefCnt
);
711 // IEnumFORMATETC->Next
713 STDMETHODIMP
CEnumFormatEtc::Next( ULONG nRequested
, FORMATETC
* lpDest
, ULONG
* lpFetched
)
715 if ( ( nRequested
< 1 ) ||
716 (( nRequested
> 1 ) && ( nullptr == lpFetched
)) ||
717 IsBadWritePtr( lpDest
, sizeof( FORMATETC
) * nRequested
) )
720 sal_uInt32 nFetched
= m_FormatEtcContainer
.nextFormatEtc( lpDest
, nRequested
);
722 if ( nullptr != lpFetched
)
723 *lpFetched
= nFetched
;
725 return (nFetched
== nRequested
) ? S_OK
: S_FALSE
;
728 // IEnumFORMATETC->Skip
730 STDMETHODIMP
CEnumFormatEtc::Skip( ULONG celt
)
732 return m_FormatEtcContainer
.skipFormatEtc( celt
) ? S_OK
: S_FALSE
;
735 // IEnumFORMATETC->Reset
737 STDMETHODIMP
CEnumFormatEtc::Reset( )
739 m_FormatEtcContainer
.beginEnumFormatEtc( );
743 // IEnumFORMATETC->Clone
745 STDMETHODIMP
CEnumFormatEtc::Clone( IEnumFORMATETC
** ppenum
)
747 if ( nullptr == ppenum
)
750 *ppenum
= new CEnumFormatEtc( m_lpUnkOuter
, m_FormatEtcContainer
);
751 static_cast< LPUNKNOWN
>( *ppenum
)->AddRef( );
756 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */