Line data Source code
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 :
21 : #include <vector>
22 :
23 : #include <osl/process.h>
24 : #include <osl/socket.hxx>
25 : #include <osl/mutex.hxx>
26 :
27 : #include <rtl/string.hxx>
28 : #include <rtl/ustrbuf.hxx>
29 :
30 : #include <com/sun/star/security/RuntimePermission.hpp>
31 : #include <com/sun/star/security/AllPermission.hpp>
32 : #include <com/sun/star/io/FilePermission.hpp>
33 : #include <com/sun/star/connection/SocketPermission.hpp>
34 : #include <com/sun/star/security/AccessControlException.hpp>
35 :
36 : #include "permissions.h"
37 :
38 :
39 : using namespace ::std;
40 : using namespace ::osl;
41 : using namespace ::com::sun::star;
42 : using namespace ::com::sun::star::uno;
43 :
44 : namespace stoc_sec
45 : {
46 :
47 :
48 0 : static inline sal_Int32 makeMask(
49 : OUString const & items, char const * const * strings ) SAL_THROW(())
50 : {
51 0 : sal_Int32 mask = 0;
52 :
53 0 : sal_Int32 n = 0;
54 0 : do
55 : {
56 0 : OUString item( items.getToken( 0, ',', n ).trim() );
57 0 : if ( item.isEmpty())
58 0 : continue;
59 0 : sal_Int32 nPos = 0;
60 0 : while (strings[ nPos ])
61 : {
62 0 : if (item.equalsAscii( strings[ nPos ] ))
63 : {
64 0 : mask |= (0x80000000 >> nPos);
65 0 : break;
66 : }
67 0 : ++nPos;
68 0 : }
69 : #if OSL_DEBUG_LEVEL > 0
70 : if (! strings[ nPos ])
71 : {
72 : OUStringBuffer buf( 48 );
73 : buf.append( "### ignoring unknown socket action: " );
74 : buf.append( item );
75 : OString str( OUStringToOString(
76 : buf.makeStringAndClear(), RTL_TEXTENCODING_ASCII_US ) );
77 : OSL_TRACE( "%s", str.getStr() );
78 : }
79 : #endif
80 : }
81 0 : while (n >= 0); // all items
82 0 : return mask;
83 : }
84 :
85 0 : static inline OUString makeStrings(
86 : sal_Int32 mask, char const * const * strings ) SAL_THROW(())
87 : {
88 0 : OUStringBuffer buf( 48 );
89 0 : while (mask)
90 : {
91 0 : if (0x80000000 & mask)
92 : {
93 0 : buf.appendAscii( *strings );
94 0 : if (mask << 1) // more items following
95 0 : buf.append( ',' );
96 : }
97 0 : mask = (mask << 1);
98 0 : ++strings;
99 : }
100 0 : return buf.makeStringAndClear();
101 : }
102 :
103 :
104 :
105 :
106 0 : class SocketPermission : public Permission
107 : {
108 : static char const * s_actions [];
109 : sal_Int32 m_actions;
110 :
111 : OUString m_host;
112 : sal_Int32 m_lowerPort;
113 : sal_Int32 m_upperPort;
114 : mutable OUString m_ip;
115 : mutable bool m_resolveErr;
116 : mutable bool m_resolvedHost;
117 : bool m_wildCardHost;
118 :
119 : inline bool resolveHost() const SAL_THROW(());
120 :
121 : public:
122 : SocketPermission(
123 : connection::SocketPermission const & perm,
124 : ::rtl::Reference< Permission > const & next = ::rtl::Reference< Permission >() )
125 : SAL_THROW(());
126 : virtual bool implies( Permission const & perm ) const SAL_THROW(()) SAL_OVERRIDE;
127 : virtual OUString toString() const SAL_THROW(()) SAL_OVERRIDE;
128 : };
129 :
130 : char const * SocketPermission::s_actions [] = { "accept", "connect", "listen", "resolve", 0 };
131 :
132 0 : SocketPermission::SocketPermission(
133 : connection::SocketPermission const & perm,
134 : ::rtl::Reference< Permission > const & next )
135 : SAL_THROW(())
136 : : Permission( SOCKET, next )
137 0 : , m_actions( makeMask( perm.Actions, s_actions ) )
138 : , m_host( perm.Host )
139 : , m_lowerPort( 0 )
140 : , m_upperPort( 65535 )
141 : , m_resolveErr( false )
142 : , m_resolvedHost( false )
143 0 : , m_wildCardHost( !perm.Host.isEmpty() && '*' == perm.Host.pData->buffer[ 0 ] )
144 : {
145 0 : if (0xe0000000 & m_actions) // if any (except resolve) is given => resolve implied
146 0 : m_actions |= 0x10000000;
147 :
148 : // separate host from portrange
149 0 : sal_Int32 colon = m_host.indexOf( ':' );
150 0 : if (colon >= 0) // port [range] given
151 : {
152 0 : sal_Int32 minus = m_host.indexOf( '-', colon +1 );
153 0 : if (minus < 0)
154 : {
155 0 : m_lowerPort = m_upperPort = m_host.copy( colon +1 ).toInt32();
156 : }
157 0 : else if (minus == (colon +1)) // -N
158 : {
159 0 : m_upperPort = m_host.copy( minus +1 ).toInt32();
160 : }
161 0 : else if (minus == (m_host.getLength() -1)) // N-
162 : {
163 0 : m_lowerPort = m_host.copy( colon +1, m_host.getLength() -1 -colon -1 ).toInt32();
164 : }
165 : else // A-B
166 : {
167 0 : m_lowerPort = m_host.copy( colon +1, minus - colon -1 ).toInt32();
168 0 : m_upperPort = m_host.copy( minus +1, m_host.getLength() -minus -1 ).toInt32();
169 : }
170 0 : m_host = m_host.copy( 0, colon );
171 : }
172 0 : }
173 :
174 0 : inline bool SocketPermission::resolveHost() const SAL_THROW(())
175 : {
176 0 : if (m_resolveErr)
177 0 : return false;
178 :
179 0 : if (! m_resolvedHost)
180 : {
181 : // dns lookup
182 0 : SocketAddr addr;
183 0 : SocketAddr::resolveHostname( m_host, addr );
184 0 : OUString ip;
185 0 : m_resolveErr = (::osl_Socket_Ok != ::osl_getDottedInetAddrOfSocketAddr(
186 0 : addr.getHandle(), &ip.pData ));
187 0 : if (m_resolveErr)
188 0 : return false;
189 :
190 0 : MutexGuard guard( Mutex::getGlobalMutex() );
191 0 : if (! m_resolvedHost)
192 : {
193 0 : m_ip = ip;
194 0 : m_resolvedHost = true;
195 0 : }
196 : }
197 0 : return m_resolvedHost;
198 : }
199 :
200 0 : bool SocketPermission::implies( Permission const & perm ) const SAL_THROW(())
201 : {
202 : // check type
203 0 : if (SOCKET != perm.m_type)
204 0 : return false;
205 0 : SocketPermission const & demanded = static_cast< SocketPermission const & >( perm );
206 :
207 : // check actions
208 0 : if ((m_actions & demanded.m_actions) != demanded.m_actions)
209 0 : return false;
210 :
211 : // check ports
212 0 : if (demanded.m_lowerPort < m_lowerPort)
213 0 : return false;
214 0 : if (demanded.m_upperPort > m_upperPort)
215 0 : return false;
216 :
217 : // quick check host (DNS names: RFC 1034/1035)
218 0 : if (m_host.equalsIgnoreAsciiCase( demanded.m_host ))
219 0 : return true;
220 : // check for host wildcards
221 0 : if (m_wildCardHost)
222 : {
223 0 : OUString const & demanded_host = demanded.m_host;
224 0 : if (demanded_host.getLength() <= m_host.getLength())
225 0 : return false;
226 0 : sal_Int32 len = m_host.getLength() -1; // skip star
227 : return (0 == ::rtl_ustr_compareIgnoreAsciiCase_WithLength(
228 0 : demanded_host.getStr() + demanded_host.getLength() - len, len,
229 0 : m_host.pData->buffer + 1, len ));
230 : }
231 0 : if (demanded.m_wildCardHost)
232 0 : return false;
233 :
234 : // compare IP addresses
235 0 : if (! resolveHost())
236 0 : return false;
237 0 : if (! demanded.resolveHost())
238 0 : return false;
239 0 : return m_ip.equals( demanded.m_ip );
240 : }
241 :
242 0 : OUString SocketPermission::toString() const SAL_THROW(())
243 : {
244 0 : OUStringBuffer buf( 48 );
245 : // host
246 0 : buf.append( "com.sun.star.connection.SocketPermission (host=\"" );
247 0 : buf.append( m_host );
248 0 : if (m_resolvedHost)
249 : {
250 0 : buf.append( '[' );
251 0 : buf.append( m_ip );
252 0 : buf.append( ']' );
253 : }
254 : // port
255 0 : if (0 != m_lowerPort || 65535 != m_upperPort)
256 : {
257 0 : buf.append( ':' );
258 0 : if (m_lowerPort > 0)
259 0 : buf.append( m_lowerPort );
260 0 : if (m_upperPort > m_lowerPort)
261 : {
262 0 : buf.append( '-' );
263 0 : if (m_upperPort < 65535)
264 0 : buf.append( m_upperPort );
265 : }
266 : }
267 : // actions
268 0 : buf.append( "\", actions=\"" );
269 0 : buf.append( makeStrings( m_actions, s_actions ) );
270 0 : buf.append( "\")" );
271 0 : return buf.makeStringAndClear();
272 : }
273 :
274 :
275 :
276 :
277 0 : class FilePermission : public Permission
278 : {
279 : static char const * s_actions [];
280 : sal_Int32 m_actions;
281 :
282 : OUString m_url;
283 : bool m_allFiles;
284 :
285 : public:
286 : FilePermission(
287 : io::FilePermission const & perm,
288 : ::rtl::Reference< Permission > const & next = ::rtl::Reference< Permission >() )
289 : SAL_THROW(());
290 : virtual bool implies( Permission const & perm ) const SAL_THROW(()) SAL_OVERRIDE;
291 : virtual OUString toString() const SAL_THROW(()) SAL_OVERRIDE;
292 : };
293 :
294 : char const * FilePermission::s_actions [] = { "read", "write", "execute", "delete", 0 };
295 :
296 0 : static OUString const & getWorkingDir() SAL_THROW(())
297 : {
298 : static OUString * s_workingDir = 0;
299 0 : if (! s_workingDir)
300 : {
301 0 : OUString workingDir;
302 0 : ::osl_getProcessWorkingDir( &workingDir.pData );
303 :
304 0 : MutexGuard guard( Mutex::getGlobalMutex() );
305 0 : if (! s_workingDir)
306 : {
307 0 : static OUString s_dir( workingDir );
308 0 : s_workingDir = &s_dir;
309 0 : }
310 : }
311 0 : return *s_workingDir;
312 : }
313 :
314 0 : FilePermission::FilePermission(
315 : io::FilePermission const & perm,
316 : ::rtl::Reference< Permission > const & next )
317 : SAL_THROW(())
318 : : Permission( FILE, next )
319 0 : , m_actions( makeMask( perm.Actions, s_actions ) )
320 : , m_url( perm.URL )
321 0 : , m_allFiles( perm.URL == "<<ALL FILES>>" )
322 : {
323 0 : if (! m_allFiles)
324 : {
325 0 : if ( m_url == "*" )
326 : {
327 0 : OUStringBuffer buf( 64 );
328 0 : buf.append( getWorkingDir() );
329 0 : buf.append( "/*" );
330 0 : m_url = buf.makeStringAndClear();
331 : }
332 0 : else if ( m_url == "-" )
333 : {
334 0 : OUStringBuffer buf( 64 );
335 0 : buf.append( getWorkingDir() );
336 0 : buf.append( "/-" );
337 0 : m_url = buf.makeStringAndClear();
338 : }
339 0 : else if (!m_url.startsWith("file:///"))
340 : {
341 : // relative path
342 0 : OUString out;
343 : oslFileError rc = ::osl_getAbsoluteFileURL(
344 0 : getWorkingDir().pData, perm.URL.pData, &out.pData );
345 0 : m_url = (osl_File_E_None == rc ? out : perm.URL); // fallback
346 : }
347 : #ifdef SAL_W32
348 : // correct win drive letters
349 : if (9 < m_url.getLength() && '|' == m_url[ 9 ]) // file:///X|
350 : {
351 : static OUString s_colon = ":";
352 : // common case in API is a ':' (sal), so convert '|' to ':'
353 : m_url = m_url.replaceAt( 9, 1, s_colon );
354 : }
355 : #endif
356 : }
357 0 : }
358 :
359 0 : bool FilePermission::implies( Permission const & perm ) const SAL_THROW(())
360 : {
361 : // check type
362 0 : if (FILE != perm.m_type)
363 0 : return false;
364 0 : FilePermission const & demanded = static_cast< FilePermission const & >( perm );
365 :
366 : // check actions
367 0 : if ((m_actions & demanded.m_actions) != demanded.m_actions)
368 0 : return false;
369 :
370 : // check url
371 0 : if (m_allFiles)
372 0 : return true;
373 0 : if (demanded.m_allFiles)
374 0 : return false;
375 :
376 : #ifdef SAL_W32
377 : if (m_url.equalsIgnoreAsciiCase( demanded.m_url ))
378 : return true;
379 : #else
380 0 : if (m_url.equals( demanded.m_url ))
381 0 : return true;
382 : #endif
383 0 : if (m_url.getLength() > demanded.m_url.getLength())
384 0 : return false;
385 : // check /- wildcard: all files and recursive in that path
386 0 : if (1 < m_url.getLength() &&
387 0 : 0 == ::rtl_ustr_ascii_compare_WithLength( m_url.getStr() + m_url.getLength() - 2, 2, "/-" ))
388 : {
389 : // demanded url must start with granted path (including path trailing path sep)
390 0 : sal_Int32 len = m_url.getLength() -1;
391 : #ifdef SAL_W32
392 : return (0 == ::rtl_ustr_compareIgnoreAsciiCase_WithLength(
393 : demanded.m_url.pData->buffer, len, m_url.pData->buffer, len ));
394 : #else
395 : return (0 == ::rtl_ustr_reverseCompare_WithLength(
396 0 : demanded.m_url.pData->buffer, len, m_url.pData->buffer, len ));
397 : #endif
398 : }
399 : // check /* wildcard: all files in that path (not recursive!)
400 0 : if (1 < m_url.getLength() &&
401 0 : 0 == ::rtl_ustr_ascii_compare_WithLength( m_url.getStr() + m_url.getLength() - 2, 2, "/*" ))
402 : {
403 : // demanded url must start with granted path (including path trailing path sep)
404 0 : sal_Int32 len = m_url.getLength() -1;
405 : #ifdef SAL_W32
406 : return ((0 == ::rtl_ustr_compareIgnoreAsciiCase_WithLength(
407 : demanded.m_url.pData->buffer, len, m_url.pData->buffer, len )) &&
408 : (0 > demanded.m_url.indexOf( '/', len ))); // in addition, no deeper paths
409 : #else
410 : return ((0 == ::rtl_ustr_reverseCompare_WithLength(
411 0 : demanded.m_url.pData->buffer, len, m_url.pData->buffer, len )) &&
412 0 : (0 > demanded.m_url.indexOf( '/', len ))); // in addition, no deeper paths
413 : #endif
414 : }
415 0 : return false;
416 : }
417 :
418 0 : OUString FilePermission::toString() const SAL_THROW(())
419 : {
420 0 : OUStringBuffer buf( 48 );
421 : // url
422 0 : buf.append( "com.sun.star.io.FilePermission (url=\"" );
423 0 : buf.append( m_url );
424 : // actions
425 0 : buf.append( "\", actions=\"" );
426 0 : buf.append( makeStrings( m_actions, s_actions ) );
427 0 : buf.append( "\")" );
428 0 : return buf.makeStringAndClear();
429 : }
430 :
431 :
432 :
433 :
434 0 : class RuntimePermission : public Permission
435 : {
436 : OUString m_name;
437 :
438 : public:
439 0 : inline RuntimePermission(
440 : security::RuntimePermission const & perm,
441 : ::rtl::Reference< Permission > const & next = ::rtl::Reference< Permission >() )
442 : SAL_THROW(())
443 : : Permission( RUNTIME, next )
444 0 : , m_name( perm.Name )
445 0 : {}
446 : virtual bool implies( Permission const & perm ) const SAL_THROW(()) SAL_OVERRIDE;
447 : virtual OUString toString() const SAL_THROW(()) SAL_OVERRIDE;
448 : };
449 :
450 0 : bool RuntimePermission::implies( Permission const & perm ) const SAL_THROW(())
451 : {
452 : // check type
453 0 : if (RUNTIME != perm.m_type)
454 0 : return false;
455 0 : RuntimePermission const & demanded = static_cast< RuntimePermission const & >( perm );
456 :
457 : // check name
458 0 : return m_name.equals( demanded.m_name );
459 : }
460 :
461 0 : OUString RuntimePermission::toString() const SAL_THROW(())
462 : {
463 0 : OUStringBuffer buf( 48 );
464 0 : buf.append( "com.sun.star.security.RuntimePermission (name=\"" );
465 0 : buf.append( m_name );
466 0 : buf.append( "\")" );
467 0 : return buf.makeStringAndClear();
468 : }
469 :
470 :
471 :
472 :
473 0 : bool AllPermission::implies( Permission const & ) const SAL_THROW(())
474 : {
475 0 : return true;
476 : }
477 :
478 0 : OUString AllPermission::toString() const SAL_THROW(())
479 : {
480 0 : return OUString("com.sun.star.security.AllPermission");
481 : }
482 :
483 :
484 :
485 :
486 0 : PermissionCollection::PermissionCollection(
487 : Sequence< Any > const & permissions, PermissionCollection const & addition )
488 : SAL_THROW( (RuntimeException) )
489 0 : : m_head( addition.m_head )
490 : {
491 0 : Any const * perms = permissions.getConstArray();
492 0 : for ( sal_Int32 nPos = permissions.getLength(); nPos--; )
493 : {
494 0 : Any const & perm = perms[ nPos ];
495 0 : Type const & perm_type = perm.getValueType();
496 :
497 : // supported permission types
498 0 : if (perm_type.equals( ::getCppuType( (io::FilePermission const *)0 ) ))
499 : {
500 0 : m_head = new FilePermission(
501 0 : *reinterpret_cast< io::FilePermission const * >( perm.pData ), m_head );
502 : }
503 0 : else if (perm_type.equals( ::getCppuType( (connection::SocketPermission const *)0 ) ))
504 : {
505 0 : m_head = new SocketPermission(
506 0 : *reinterpret_cast< connection::SocketPermission const * >( perm.pData ), m_head );
507 : }
508 0 : else if (perm_type.equals( ::getCppuType( (security::RuntimePermission const *)0 ) ))
509 : {
510 0 : m_head = new RuntimePermission(
511 0 : *reinterpret_cast< security::RuntimePermission const * >( perm.pData ), m_head );
512 : }
513 0 : else if (perm_type.equals( ::getCppuType( (security::AllPermission const *)0 ) ))
514 : {
515 0 : m_head = new AllPermission( m_head );
516 : }
517 : else
518 : {
519 0 : OUStringBuffer buf( 48 );
520 0 : buf.append( "checking for unsupported permission type: " );
521 0 : buf.append( perm_type.getTypeName() );
522 : throw RuntimeException(
523 0 : buf.makeStringAndClear(), Reference< XInterface >() );
524 : }
525 : }
526 0 : }
527 : #ifdef __DIAGNOSE
528 :
529 : Sequence< OUString > PermissionCollection::toStrings() const SAL_THROW(())
530 : {
531 : vector< OUString > strings;
532 : strings.reserve( 8 );
533 : for ( Permission * perm = m_head.get(); perm; perm = perm->m_next.get() )
534 : {
535 : strings.push_back( perm->toString() );
536 : }
537 : return Sequence< OUString >(
538 : strings.empty() ? 0 : &strings[ 0 ], strings.size() );
539 : }
540 : #endif
541 :
542 0 : inline static bool __implies(
543 : ::rtl::Reference< Permission > const & head, Permission const & demanded ) SAL_THROW(())
544 : {
545 0 : for ( Permission * perm = head.get(); perm; perm = perm->m_next.get() )
546 : {
547 0 : if (perm->implies( demanded ))
548 0 : return true;
549 : }
550 0 : return false;
551 : }
552 :
553 : #ifdef __DIAGNOSE
554 :
555 : static void demanded_diag(
556 : Permission const & perm )
557 : SAL_THROW(())
558 : {
559 : OUStringBuffer buf( 48 );
560 : buf.append( "demanding " );
561 : buf.append( perm.toString() );
562 : buf.append( " => ok." );
563 : OString str(
564 : OUStringToOString( buf.makeStringAndClear(), RTL_TEXTENCODING_ASCII_US ) );
565 : OSL_TRACE( "%s", str.getStr() );
566 : }
567 : #endif
568 :
569 0 : static void throwAccessControlException(
570 : Permission const & perm, Any const & demanded_perm )
571 : SAL_THROW( (security::AccessControlException) )
572 : {
573 0 : OUStringBuffer buf( 48 );
574 0 : buf.append( "access denied: " );
575 0 : buf.append( perm.toString() );
576 : throw security::AccessControlException(
577 0 : buf.makeStringAndClear(), Reference< XInterface >(), demanded_perm );
578 : }
579 :
580 0 : void PermissionCollection::checkPermission( Any const & perm ) const
581 : SAL_THROW( (RuntimeException) )
582 : {
583 0 : Type const & demanded_type = perm.getValueType();
584 :
585 : // supported permission types
586 : // stack object of SimpleReferenceObject are ok, as long as they are not
587 : // assigned to a ::rtl::Reference<> (=> delete this)
588 0 : if (demanded_type.equals( ::getCppuType( (io::FilePermission const *)0 ) ))
589 : {
590 : FilePermission demanded(
591 0 : *reinterpret_cast< io::FilePermission const * >( perm.pData ) );
592 0 : if (__implies( m_head, demanded ))
593 : {
594 : #ifdef __DIAGNOSE
595 : demanded_diag( demanded );
596 : #endif
597 0 : return;
598 : }
599 0 : throwAccessControlException( demanded, perm );
600 : }
601 0 : else if (demanded_type.equals( ::getCppuType( (connection::SocketPermission const *)0 ) ))
602 : {
603 : SocketPermission demanded(
604 0 : *reinterpret_cast< connection::SocketPermission const * >( perm.pData ) );
605 0 : if (__implies( m_head, demanded ))
606 : {
607 : #ifdef __DIAGNOSE
608 : demanded_diag( demanded );
609 : #endif
610 0 : return;
611 : }
612 0 : throwAccessControlException( demanded, perm );
613 : }
614 0 : else if (demanded_type.equals( ::getCppuType( (security::RuntimePermission const *)0 ) ))
615 : {
616 : RuntimePermission demanded(
617 0 : *reinterpret_cast< security::RuntimePermission const * >( perm.pData ) );
618 0 : if (__implies( m_head, demanded ))
619 : {
620 : #ifdef __DIAGNOSE
621 : demanded_diag( demanded );
622 : #endif
623 0 : return;
624 : }
625 0 : throwAccessControlException( demanded, perm );
626 : }
627 0 : else if (demanded_type.equals( ::getCppuType( (security::AllPermission const *)0 ) ))
628 : {
629 0 : AllPermission demanded;
630 0 : if (__implies( m_head, demanded ))
631 : {
632 : #ifdef __DIAGNOSE
633 : demanded_diag( demanded );
634 : #endif
635 0 : return;
636 : }
637 0 : throwAccessControlException( demanded, perm );
638 : }
639 : else
640 : {
641 0 : OUStringBuffer buf( 48 );
642 0 : buf.append( "checking for unsupported permission type: " );
643 0 : buf.append( demanded_type.getTypeName() );
644 : throw RuntimeException(
645 0 : buf.makeStringAndClear(), Reference< XInterface >() );
646 : }
647 : }
648 :
649 : }
650 :
651 : /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|