VirtualBox

source: vbox/trunk/src/VBox/Frontends/VBoxManage/VBoxManageUSB.cpp@ 47376

Last change on this file since 47376 was 47376, checked in by vboxsync, 11 years ago

Main/USB: USB Controller implementation rework. Moved filter handling into a separate interface

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 21.7 KB
Line 
1/* $Id: VBoxManageUSB.cpp 47376 2013-07-24 15:13:52Z vboxsync $ */
2/** @file
3 * VBoxManage - VirtualBox's command-line interface.
4 */
5
6/*
7 * Copyright (C) 2006-2013 Oracle Corporation
8 *
9 * This file is part of VirtualBox Open Source Edition (OSE), as
10 * available from http://www.virtualbox.org. This file is free software;
11 * you can redistribute it and/or modify it under the terms of the GNU
12 * General Public License (GPL) as published by the Free Software
13 * Foundation, in version 2 as it comes in the "COPYING" file of the
14 * VirtualBox OSE distribution. VirtualBox OSE is distributed in the
15 * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
16 */
17
18#include <VBox/com/com.h>
19#include <VBox/com/string.h>
20#include <VBox/com/Guid.h>
21#include <VBox/com/array.h>
22#include <VBox/com/ErrorInfo.h>
23#include <VBox/com/errorprint.h>
24#include <VBox/com/VirtualBox.h>
25
26#include "VBoxManage.h"
27
28#include <iprt/asm.h>
29
30/* missing XPCOM <-> COM wrappers */
31#ifndef STDMETHOD_
32# define STDMETHOD_(ret, meth) NS_IMETHOD_(ret) meth
33#endif
34#ifndef NS_GET_IID
35# define NS_GET_IID(I) IID_##I
36#endif
37#ifndef RT_OS_WINDOWS
38#define IUnknown nsISupports
39#endif
40
41using namespace com;
42
43/**
44 * Quick IUSBDevice implementation for detaching / attaching
45 * devices to the USB Controller.
46 */
47class MyUSBDevice : public IUSBDevice
48{
49public:
50 // public initializer/uninitializer for internal purposes only
51 MyUSBDevice(uint16_t a_u16VendorId, uint16_t a_u16ProductId, uint16_t a_bcdRevision, uint64_t a_u64SerialHash, const char *a_pszComment)
52 : m_usVendorId(a_u16VendorId), m_usProductId(a_u16ProductId),
53 m_bcdRevision(a_bcdRevision), m_u64SerialHash(a_u64SerialHash),
54 m_bstrComment(a_pszComment),
55 m_cRefs(0)
56 {
57 }
58
59 STDMETHOD_(ULONG, AddRef)(void)
60 {
61 return ASMAtomicIncU32(&m_cRefs);
62 }
63 STDMETHOD_(ULONG, Release)(void)
64 {
65 ULONG cRefs = ASMAtomicDecU32(&m_cRefs);
66 if (!cRefs)
67 delete this;
68 return cRefs;
69 }
70 STDMETHOD(QueryInterface)(const IID &iid, void **ppvObject)
71 {
72 Guid guid(iid);
73 if (guid == Guid(NS_GET_IID(IUnknown)))
74 *ppvObject = (IUnknown *)this;
75#ifdef RT_OS_WINDOWS
76 else if (guid == Guid(NS_GET_IID(IDispatch)))
77 *ppvObject = (IDispatch *)this;
78#endif
79 else if (guid == Guid(NS_GET_IID(IUSBDevice)))
80 *ppvObject = (IUSBDevice *)this;
81 else
82 return E_NOINTERFACE;
83 AddRef();
84 return S_OK;
85 }
86
87 STDMETHOD(COMGETTER(Id))(OUT_GUID a_pId) { return E_NOTIMPL; }
88 STDMETHOD(COMGETTER(VendorId))(USHORT *a_pusVendorId) { *a_pusVendorId = m_usVendorId; return S_OK; }
89 STDMETHOD(COMGETTER(ProductId))(USHORT *a_pusProductId) { *a_pusProductId = m_usProductId; return S_OK; }
90 STDMETHOD(COMGETTER(Revision))(USHORT *a_pusRevision) { *a_pusRevision = m_bcdRevision; return S_OK; }
91 STDMETHOD(COMGETTER(SerialHash))(ULONG64 *a_pullSerialHash) { *a_pullSerialHash = m_u64SerialHash; return S_OK; }
92 STDMETHOD(COMGETTER(Manufacturer))(BSTR *a_pManufacturer) { return E_NOTIMPL; }
93 STDMETHOD(COMGETTER(Product))(BSTR *a_pProduct) { return E_NOTIMPL; }
94 STDMETHOD(COMGETTER(SerialNumber))(BSTR *a_pSerialNumber) { return E_NOTIMPL; }
95 STDMETHOD(COMGETTER(Address))(BSTR *a_pAddress) { return E_NOTIMPL; }
96
97private:
98 /** The vendor id of this USB device. */
99 USHORT m_usVendorId;
100 /** The product id of this USB device. */
101 USHORT m_usProductId;
102 /** The product revision number of this USB device.
103 * (high byte = integer; low byte = decimal) */
104 USHORT m_bcdRevision;
105 /** The USB serial hash of the device. */
106 uint64_t m_u64SerialHash;
107 /** The user comment string. */
108 Bstr m_bstrComment;
109 /** Reference counter. */
110 uint32_t volatile m_cRefs;
111};
112
113
114// types
115///////////////////////////////////////////////////////////////////////////////
116
117template <typename T>
118class Nullable
119{
120public:
121
122 Nullable() : mIsNull(true) {}
123 Nullable(const T &aValue, bool aIsNull = false)
124 : mIsNull(aIsNull), mValue(aValue) {}
125
126 bool isNull() const { return mIsNull; };
127 void setNull(bool aIsNull = true) { mIsNull = aIsNull; }
128
129 operator const T&() const { return mValue; }
130
131 Nullable &operator= (const T &aValue)
132 {
133 mValue = aValue;
134 mIsNull = false;
135 return *this;
136 }
137
138private:
139
140 bool mIsNull;
141 T mValue;
142};
143
144/** helper structure to encapsulate USB filter manipulation commands */
145struct USBFilterCmd
146{
147 struct USBFilter
148 {
149 USBFilter()
150 : mAction(USBDeviceFilterAction_Null)
151 {}
152
153 Bstr mName;
154 Nullable <bool> mActive;
155 Bstr mVendorId;
156 Bstr mProductId;
157 Bstr mRevision;
158 Bstr mManufacturer;
159 Bstr mProduct;
160 Bstr mRemote;
161 Bstr mSerialNumber;
162 Nullable <ULONG> mMaskedInterfaces;
163 USBDeviceFilterAction_T mAction;
164 };
165
166 enum Action { Invalid, Add, Modify, Remove };
167
168 USBFilterCmd() : mAction(Invalid), mIndex(0), mGlobal(false) {}
169
170 Action mAction;
171 uint32_t mIndex;
172 /** flag whether the command target is a global filter */
173 bool mGlobal;
174 /** machine this command is targeted at (null for global filters) */
175 ComPtr<IMachine> mMachine;
176 USBFilter mFilter;
177};
178
179int handleUSBFilter(HandlerArg *a)
180{
181 HRESULT rc = S_OK;
182 USBFilterCmd cmd;
183
184 /* at least: 0: command, 1: index, 2: --target, 3: <target value> */
185 if (a->argc < 4)
186 return errorSyntax(USAGE_USBFILTER, "Not enough parameters");
187
188 /* which command? */
189 cmd.mAction = USBFilterCmd::Invalid;
190 if (!strcmp(a->argv[0], "add")) cmd.mAction = USBFilterCmd::Add;
191 else if (!strcmp(a->argv[0], "modify")) cmd.mAction = USBFilterCmd::Modify;
192 else if (!strcmp(a->argv[0], "remove")) cmd.mAction = USBFilterCmd::Remove;
193
194 if (cmd.mAction == USBFilterCmd::Invalid)
195 return errorSyntax(USAGE_USBFILTER, "Invalid parameter '%s'", a->argv[0]);
196
197 /* which index? */
198 if (VINF_SUCCESS != RTStrToUInt32Full(a->argv[1], 10, &cmd.mIndex))
199 return errorSyntax(USAGE_USBFILTER, "Invalid index '%s'", a->argv[1]);
200
201 switch (cmd.mAction)
202 {
203 case USBFilterCmd::Add:
204 case USBFilterCmd::Modify:
205 {
206 /* at least: 0: command, 1: index, 2: --target, 3: <target value>, 4: --name, 5: <name value> */
207 if (a->argc < 6)
208 {
209 if (cmd.mAction == USBFilterCmd::Add)
210 return errorSyntax(USAGE_USBFILTER_ADD, "Not enough parameters");
211
212 return errorSyntax(USAGE_USBFILTER_MODIFY, "Not enough parameters");
213 }
214
215 // set Active to true by default
216 // (assuming that the user sets up all necessary attributes
217 // at once and wants the filter to be active immediately)
218 if (cmd.mAction == USBFilterCmd::Add)
219 cmd.mFilter.mActive = true;
220
221 for (int i = 2; i < a->argc; i++)
222 {
223 if ( !strcmp(a->argv[i], "--target")
224 || !strcmp(a->argv[i], "-target"))
225 {
226 if (a->argc <= i + 1 || !*a->argv[i+1])
227 return errorArgument("Missing argument to '%s'", a->argv[i]);
228 i++;
229 if (!strcmp(a->argv[i], "global"))
230 cmd.mGlobal = true;
231 else
232 {
233 /* assume it's a UUID of a machine */
234 CHECK_ERROR_RET(a->virtualBox, FindMachine(Bstr(a->argv[i]).raw(),
235 cmd.mMachine.asOutParam()), 1);
236 }
237 }
238 else if ( !strcmp(a->argv[i], "--name")
239 || !strcmp(a->argv[i], "-name"))
240 {
241 if (a->argc <= i + 1 || !*a->argv[i+1])
242 return errorArgument("Missing argument to '%s'", a->argv[i]);
243 i++;
244 cmd.mFilter.mName = a->argv[i];
245 }
246 else if ( !strcmp(a->argv[i], "--active")
247 || !strcmp(a->argv[i], "-active"))
248 {
249 if (a->argc <= i + 1)
250 return errorArgument("Missing argument to '%s'", a->argv[i]);
251 i++;
252 if (!strcmp(a->argv[i], "yes"))
253 cmd.mFilter.mActive = true;
254 else if (!strcmp(a->argv[i], "no"))
255 cmd.mFilter.mActive = false;
256 else
257 return errorArgument("Invalid --active argument '%s'", a->argv[i]);
258 }
259 else if ( !strcmp(a->argv[i], "--vendorid")
260 || !strcmp(a->argv[i], "-vendorid"))
261 {
262 if (a->argc <= i + 1)
263 return errorArgument("Missing argument to '%s'", a->argv[i]);
264 i++;
265 cmd.mFilter.mVendorId = a->argv[i];
266 }
267 else if ( !strcmp(a->argv[i], "--productid")
268 || !strcmp(a->argv[i], "-productid"))
269 {
270 if (a->argc <= i + 1)
271 return errorArgument("Missing argument to '%s'", a->argv[i]);
272 i++;
273 cmd.mFilter.mProductId = a->argv[i];
274 }
275 else if ( !strcmp(a->argv[i], "--revision")
276 || !strcmp(a->argv[i], "-revision"))
277 {
278 if (a->argc <= i + 1)
279 return errorArgument("Missing argument to '%s'", a->argv[i]);
280 i++;
281 cmd.mFilter.mRevision = a->argv[i];
282 }
283 else if ( !strcmp(a->argv[i], "--manufacturer")
284 || !strcmp(a->argv[i], "-manufacturer"))
285 {
286 if (a->argc <= i + 1)
287 return errorArgument("Missing argument to '%s'", a->argv[i]);
288 i++;
289 cmd.mFilter.mManufacturer = a->argv[i];
290 }
291 else if ( !strcmp(a->argv[i], "--product")
292 || !strcmp(a->argv[i], "-product"))
293 {
294 if (a->argc <= i + 1)
295 return errorArgument("Missing argument to '%s'", a->argv[i]);
296 i++;
297 cmd.mFilter.mProduct = a->argv[i];
298 }
299 else if ( !strcmp(a->argv[i], "--remote")
300 || !strcmp(a->argv[i], "-remote"))
301 {
302 if (a->argc <= i + 1)
303 return errorArgument("Missing argument to '%s'", a->argv[i]);
304 i++;
305 cmd.mFilter.mRemote = a->argv[i];
306 }
307 else if ( !strcmp(a->argv[i], "--serialnumber")
308 || !strcmp(a->argv[i], "-serialnumber"))
309 {
310 if (a->argc <= i + 1)
311 return errorArgument("Missing argument to '%s'", a->argv[i]);
312 i++;
313 cmd.mFilter.mSerialNumber = a->argv[i];
314 }
315 else if ( !strcmp(a->argv[i], "--maskedinterfaces")
316 || !strcmp(a->argv[i], "-maskedinterfaces"))
317 {
318 if (a->argc <= i + 1)
319 return errorArgument("Missing argument to '%s'", a->argv[i]);
320 i++;
321 uint32_t u32;
322 int vrc = RTStrToUInt32Full(a->argv[i], 0, &u32);
323 if (RT_FAILURE(vrc))
324 return errorArgument("Failed to convert the --maskedinterfaces value '%s' to a number, vrc=%Rrc", a->argv[i], vrc);
325 cmd.mFilter.mMaskedInterfaces = u32;
326 }
327 else if ( !strcmp(a->argv[i], "--action")
328 || !strcmp(a->argv[i], "-action"))
329 {
330 if (a->argc <= i + 1)
331 return errorArgument("Missing argument to '%s'", a->argv[i]);
332 i++;
333 if (!strcmp(a->argv[i], "ignore"))
334 cmd.mFilter.mAction = USBDeviceFilterAction_Ignore;
335 else if (!strcmp(a->argv[i], "hold"))
336 cmd.mFilter.mAction = USBDeviceFilterAction_Hold;
337 else
338 return errorArgument("Invalid USB filter action '%s'", a->argv[i]);
339 }
340 else
341 return errorSyntax(cmd.mAction == USBFilterCmd::Add ? USAGE_USBFILTER_ADD : USAGE_USBFILTER_MODIFY,
342 "Unknown option '%s'", a->argv[i]);
343 }
344
345 if (cmd.mAction == USBFilterCmd::Add)
346 {
347 // mandatory/forbidden options
348 if ( cmd.mFilter.mName.isEmpty()
349 ||
350 ( cmd.mGlobal
351 && cmd.mFilter.mAction == USBDeviceFilterAction_Null
352 )
353 || ( !cmd.mGlobal
354 && !cmd.mMachine)
355 || ( cmd.mGlobal
356 && !cmd.mFilter.mRemote.isEmpty())
357 )
358 {
359 return errorSyntax(USAGE_USBFILTER_ADD, "Mandatory options not supplied");
360 }
361 }
362 break;
363 }
364
365 case USBFilterCmd::Remove:
366 {
367 /* at least: 0: command, 1: index, 2: --target, 3: <target value> */
368 if (a->argc < 4)
369 return errorSyntax(USAGE_USBFILTER_REMOVE, "Not enough parameters");
370
371 for (int i = 2; i < a->argc; i++)
372 {
373 if ( !strcmp(a->argv[i], "--target")
374 || !strcmp(a->argv[i], "-target"))
375 {
376 if (a->argc <= i + 1 || !*a->argv[i+1])
377 return errorArgument("Missing argument to '%s'", a->argv[i]);
378 i++;
379 if (!strcmp(a->argv[i], "global"))
380 cmd.mGlobal = true;
381 else
382 {
383 CHECK_ERROR_RET(a->virtualBox, FindMachine(Bstr(a->argv[i]).raw(),
384 cmd.mMachine.asOutParam()), 1);
385 }
386 }
387 }
388
389 // mandatory options
390 if (!cmd.mGlobal && !cmd.mMachine)
391 return errorSyntax(USAGE_USBFILTER_REMOVE, "Mandatory options not supplied");
392
393 break;
394 }
395
396 default: break;
397 }
398
399 USBFilterCmd::USBFilter &f = cmd.mFilter;
400
401 ComPtr <IHost> host;
402 ComPtr <IUSBDeviceFilters> flts;
403 if (cmd.mGlobal)
404 CHECK_ERROR_RET(a->virtualBox, COMGETTER(Host)(host.asOutParam()), 1);
405 else
406 {
407 /* open a session for the VM */
408 CHECK_ERROR_RET(cmd.mMachine, LockMachine(a->session, LockType_Shared), 1);
409 /* get the mutable session machine */
410 a->session->COMGETTER(Machine)(cmd.mMachine.asOutParam());
411 /* and get the USB device filters */
412 CHECK_ERROR_RET(cmd.mMachine, COMGETTER(USBDeviceFilters)(flts.asOutParam()), 1);
413 }
414
415 switch (cmd.mAction)
416 {
417 case USBFilterCmd::Add:
418 {
419 if (cmd.mGlobal)
420 {
421 ComPtr <IHostUSBDeviceFilter> flt;
422 CHECK_ERROR_BREAK(host, CreateUSBDeviceFilter(f.mName.raw(),
423 flt.asOutParam()));
424
425 if (!f.mActive.isNull())
426 CHECK_ERROR_BREAK(flt, COMSETTER(Active)(f.mActive));
427 if (!f.mVendorId.isEmpty())
428 CHECK_ERROR_BREAK(flt, COMSETTER(VendorId)(f.mVendorId.raw()));
429 if (!f.mProductId.isEmpty())
430 CHECK_ERROR_BREAK(flt, COMSETTER(ProductId)(f.mProductId.raw()));
431 if (!f.mRevision.isEmpty())
432 CHECK_ERROR_BREAK(flt, COMSETTER(Revision)(f.mRevision.raw()));
433 if (!f.mManufacturer.isEmpty())
434 CHECK_ERROR_BREAK(flt, COMSETTER(Manufacturer)(f.mManufacturer.raw()));
435 if (!f.mSerialNumber.isEmpty())
436 CHECK_ERROR_BREAK(flt, COMSETTER(SerialNumber)(f.mSerialNumber.raw()));
437 if (!f.mMaskedInterfaces.isNull())
438 CHECK_ERROR_BREAK(flt, COMSETTER(MaskedInterfaces)(f.mMaskedInterfaces));
439
440 if (f.mAction != USBDeviceFilterAction_Null)
441 CHECK_ERROR_BREAK(flt, COMSETTER(Action)(f.mAction));
442
443 CHECK_ERROR_BREAK(host, InsertUSBDeviceFilter(cmd.mIndex, flt));
444 }
445 else
446 {
447 ComPtr <IUSBDeviceFilter> flt;
448 CHECK_ERROR_BREAK(flts, CreateDeviceFilter(f.mName.raw(),
449 flt.asOutParam()));
450
451 if (!f.mActive.isNull())
452 CHECK_ERROR_BREAK(flt, COMSETTER(Active)(f.mActive));
453 if (!f.mVendorId.isEmpty())
454 CHECK_ERROR_BREAK(flt, COMSETTER(VendorId)(f.mVendorId.raw()));
455 if (!f.mProductId.isEmpty())
456 CHECK_ERROR_BREAK(flt, COMSETTER(ProductId)(f.mProductId.raw()));
457 if (!f.mRevision.isEmpty())
458 CHECK_ERROR_BREAK(flt, COMSETTER(Revision)(f.mRevision.raw()));
459 if (!f.mManufacturer.isEmpty())
460 CHECK_ERROR_BREAK(flt, COMSETTER(Manufacturer)(f.mManufacturer.raw()));
461 if (!f.mRemote.isEmpty())
462 CHECK_ERROR_BREAK(flt, COMSETTER(Remote)(f.mRemote.raw()));
463 if (!f.mSerialNumber.isEmpty())
464 CHECK_ERROR_BREAK(flt, COMSETTER(SerialNumber)(f.mSerialNumber.raw()));
465 if (!f.mMaskedInterfaces.isNull())
466 CHECK_ERROR_BREAK(flt, COMSETTER(MaskedInterfaces)(f.mMaskedInterfaces));
467
468 CHECK_ERROR_BREAK(flts, InsertDeviceFilter(cmd.mIndex, flt));
469 }
470 break;
471 }
472 case USBFilterCmd::Modify:
473 {
474 if (cmd.mGlobal)
475 {
476 SafeIfaceArray <IHostUSBDeviceFilter> coll;
477 CHECK_ERROR_BREAK(host, COMGETTER(USBDeviceFilters)(ComSafeArrayAsOutParam(coll)));
478
479 ComPtr <IHostUSBDeviceFilter> flt = coll[cmd.mIndex];
480
481 if (!f.mName.isEmpty())
482 CHECK_ERROR_BREAK(flt, COMSETTER(Name)(f.mName.raw()));
483 if (!f.mActive.isNull())
484 CHECK_ERROR_BREAK(flt, COMSETTER(Active)(f.mActive));
485 if (!f.mVendorId.isEmpty())
486 CHECK_ERROR_BREAK(flt, COMSETTER(VendorId)(f.mVendorId.raw()));
487 if (!f.mProductId.isEmpty())
488 CHECK_ERROR_BREAK(flt, COMSETTER(ProductId)(f.mProductId.raw()));
489 if (!f.mRevision.isEmpty())
490 CHECK_ERROR_BREAK(flt, COMSETTER(Revision)(f.mRevision.raw()));
491 if (!f.mManufacturer.isEmpty())
492 CHECK_ERROR_BREAK(flt, COMSETTER(Manufacturer)(f.mManufacturer.raw()));
493 if (!f.mSerialNumber.isEmpty())
494 CHECK_ERROR_BREAK(flt, COMSETTER(SerialNumber)(f.mSerialNumber.raw()));
495 if (!f.mMaskedInterfaces.isNull())
496 CHECK_ERROR_BREAK(flt, COMSETTER(MaskedInterfaces)(f.mMaskedInterfaces));
497
498 if (f.mAction != USBDeviceFilterAction_Null)
499 CHECK_ERROR_BREAK(flt, COMSETTER(Action)(f.mAction));
500 }
501 else
502 {
503 SafeIfaceArray <IUSBDeviceFilter> coll;
504 CHECK_ERROR_BREAK(flts, COMGETTER(DeviceFilters)(ComSafeArrayAsOutParam(coll)));
505
506 ComPtr <IUSBDeviceFilter> flt = coll[cmd.mIndex];
507
508 if (!f.mName.isEmpty())
509 CHECK_ERROR_BREAK(flt, COMSETTER(Name)(f.mName.raw()));
510 if (!f.mActive.isNull())
511 CHECK_ERROR_BREAK(flt, COMSETTER(Active)(f.mActive));
512 if (!f.mVendorId.isEmpty())
513 CHECK_ERROR_BREAK(flt, COMSETTER(VendorId)(f.mVendorId.raw()));
514 if (!f.mProductId.isEmpty())
515 CHECK_ERROR_BREAK(flt, COMSETTER(ProductId)(f.mProductId.raw()));
516 if (!f.mRevision.isEmpty())
517 CHECK_ERROR_BREAK(flt, COMSETTER(Revision)(f.mRevision.raw()));
518 if (!f.mManufacturer.isEmpty())
519 CHECK_ERROR_BREAK(flt, COMSETTER(Manufacturer)(f.mManufacturer.raw()));
520 if (!f.mRemote.isEmpty())
521 CHECK_ERROR_BREAK(flt, COMSETTER(Remote)(f.mRemote.raw()));
522 if (!f.mSerialNumber.isEmpty())
523 CHECK_ERROR_BREAK(flt, COMSETTER(SerialNumber)(f.mSerialNumber.raw()));
524 if (!f.mMaskedInterfaces.isNull())
525 CHECK_ERROR_BREAK(flt, COMSETTER(MaskedInterfaces)(f.mMaskedInterfaces));
526 }
527 break;
528 }
529 case USBFilterCmd::Remove:
530 {
531 if (cmd.mGlobal)
532 {
533 ComPtr <IHostUSBDeviceFilter> flt;
534 CHECK_ERROR_BREAK(host, RemoveUSBDeviceFilter(cmd.mIndex));
535 }
536 else
537 {
538 ComPtr <IUSBDeviceFilter> flt;
539 CHECK_ERROR_BREAK(flts, RemoveDeviceFilter(cmd.mIndex, flt.asOutParam()));
540 }
541 break;
542 }
543 default:
544 break;
545 }
546
547 if (cmd.mMachine)
548 {
549 if (SUCCEEDED(rc))
550 {
551 /* commit the session */
552 CHECK_ERROR(cmd.mMachine, SaveSettings());
553 }
554 /* close the session */
555 a->session->UnlockMachine();
556 }
557
558 return SUCCEEDED(rc) ? 0 : 1;
559}
560/* vi: set tabstop=4 shiftwidth=4 expandtab: */
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette