Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
wine-cw
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
wine
wine-cw
Commits
181a7cca
Commit
181a7cca
authored
Oct 27, 2003
by
Jukka Heinonen
Committed by
Alexandre Julliard
Oct 27, 2003
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Moved drive parameter block (DPB) routines to winedos.
parent
f227cfaa
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
246 additions
and
215 deletions
+246
-215
int21.c
dlls/winedos/int21.c
+246
-17
int21.c
msdos/int21.c
+0
-198
No files found.
dlls/winedos/int21.c
View file @
181a7cca
...
...
@@ -59,6 +59,42 @@ WINE_DEFAULT_DEBUG_CHANNEL(int21);
#include "pshpack1.h"
/*
* Extended Drive Parameter Block.
* This structure is compatible with standard DOS4+ DPB and
* extended DOS7 DPB.
*/
typedef
struct
_INT21_DPB
{
BYTE
drive
;
/* 00 drive number (0=A, ...) */
BYTE
unit
;
/* 01 unit number within device driver */
WORD
sector_bytes
;
/* 02 bytes per sector */
BYTE
cluster_sectors
;
/* 04 highest sector number within a cluster */
BYTE
shift
;
/* 05 shift count to convert clusters into sectors */
WORD
num_reserved
;
/* 06 reserved sectors at beginning of drive */
BYTE
num_FAT
;
/* 08 number of FATs */
WORD
num_root_entries
;
/* 09 number of root directory entries */
WORD
first_data_sector
;
/* 0b number of first sector containing user data */
WORD
num_clusters1
;
/* 0d highest cluster number (number of data clusters + 1) */
WORD
sectors_per_FAT
;
/* 0f number of sectors per FAT */
WORD
first_dir_sector
;
/* 11 sector number of first directory sector */
SEGPTR
driver_header
;
/* 13 address of device driver header */
BYTE
media_ID
;
/* 17 media ID byte */
BYTE
access_flag
;
/* 18 0x00 if disk accessed, 0xff if not */
SEGPTR
next
;
/* 19 pointer to next DPB */
WORD
search_cluster1
;
/* 1d cluster at which to start search for free space */
WORD
free_clusters_lo
;
/* 1f number of free clusters on drive or 0xffff if unknown */
WORD
free_clusters_hi
;
/* 21 hiword of clusters_free */
WORD
mirroring_flags
;
/* 23 active FAT/mirroring flags */
WORD
info_sector
;
/* 25 sector number of file system info sector or 0xffff for none */
WORD
spare_boot_sector
;
/* 27 sector number of backup boot sector or 0xffff for none */
DWORD
first_cluster_sector
;
/* 29 sector number of the first cluster */
DWORD
num_clusters2
;
/* 2d maximum cluster number */
DWORD
fat_clusters
;
/* 31 number of clusters occupied by FAT */
DWORD
root_cluster
;
/* 35 cluster number of start of root directory */
DWORD
search_cluster2
;
/* 39 cluster at which to start searching for free space */
}
INT21_DPB
;
/*
* Structure for DOS data that can be accessed directly from applications.
* Real and protected mode pointers will be returned to this structure so
* the structure must be correctly packed.
...
...
@@ -87,7 +123,11 @@ typedef struct _INT21_HEAP {
WORD
dbcs_size
;
/* Number of valid ranges in the following table */
BYTE
dbcs_table
[
16
];
/* Start/end bytes for N ranges and 00/00 as terminator */
BYTE
misc_indos
;
/* Interrupt 21 nesting flag */
BYTE
misc_indos
;
/* Interrupt 21 nesting flag */
WORD
misc_segment
;
/* Real mode segment for INT21_HEAP */
WORD
misc_selector
;
/* Protected mode selector for INT21_HEAP */
INT21_DPB
misc_dpb_list
[
MAX_DOS_DRIVES
];
/* Drive parameter blocks for all drives */
}
INT21_HEAP
;
...
...
@@ -353,6 +393,39 @@ static void INT21_FillHeap( INT21_HEAP *heap )
* Initialize InDos flag.
*/
heap
->
misc_indos
=
0
;
/*
* FIXME: Should drive parameter blocks (DPB) be
* initialized here and linked to DOS LOL?
*/
}
/***********************************************************************
* INT21_GetHeapPointer
*
* Get pointer for DOS heap (INT21_HEAP).
* Creates and initializes heap on first call.
*/
static
INT21_HEAP
*
INT21_GetHeapPointer
(
void
)
{
static
INT21_HEAP
*
heap_pointer
=
NULL
;
if
(
!
heap_pointer
)
{
WORD
heap_segment
;
WORD
heap_selector
;
heap_pointer
=
DOSVM_AllocDataUMB
(
sizeof
(
INT21_HEAP
),
&
heap_segment
,
&
heap_selector
);
heap_pointer
->
misc_segment
=
heap_segment
;
heap_pointer
->
misc_selector
=
heap_selector
;
INT21_FillHeap
(
heap_pointer
);
}
return
heap_pointer
;
}
...
...
@@ -364,23 +437,94 @@ static void INT21_FillHeap( INT21_HEAP *heap )
*/
static
WORD
INT21_GetHeapSelector
(
CONTEXT86
*
context
)
{
static
WORD
heap_segment
=
0
;
static
WORD
heap_selector
=
0
;
static
BOOL
heap_initialized
=
FALSE
;
INT21_HEAP
*
heap
=
INT21_GetHeapPointer
();
if
(
!
ISV86
(
context
)
&&
DOSVM_IsWin16
())
return
heap
->
misc_selector
;
else
return
heap
->
misc_segment
;
}
if
(
!
heap_initialized
)
/***********************************************************************
* INT21_FillDrivePB
*
* Fill DOS heap drive parameter block for the specified drive.
* Return TRUE if drive was valid and there were
* no errors while reading drive information.
*/
static
BOOL
INT21_FillDrivePB
(
BYTE
drive
)
{
WCHAR
drivespec
[
3
]
=
{
'A'
,
':'
,
0
};
INT21_HEAP
*
heap
=
INT21_GetHeapPointer
();
INT21_DPB
*
dpb
;
UINT
drivetype
;
DWORD
cluster_sectors
;
DWORD
sector_bytes
;
DWORD
free_clusters
;
DWORD
total_clusters
;
if
(
drive
>=
MAX_DOS_DRIVES
)
return
FALSE
;
dpb
=
&
heap
->
misc_dpb_list
[
drive
];
drivespec
[
0
]
+=
drive
;
drivetype
=
GetDriveTypeW
(
drivespec
);
/*
* FIXME: Does this check work correctly with floppy/cdrom drives?
*/
if
(
drivetype
==
DRIVE_NO_ROOT_DIR
||
drivetype
==
DRIVE_UNKNOWN
)
return
FALSE
;
/*
* FIXME: Does this check work correctly with floppy/cdrom drives?
*/
if
(
!
GetDiskFreeSpaceW
(
drivespec
,
&
cluster_sectors
,
&
sector_bytes
,
&
free_clusters
,
&
total_clusters
))
return
FALSE
;
/*
* FIXME: Most of the values listed below are incorrect.
* All values should be validated.
*/
dpb
->
drive
=
drive
;
dpb
->
unit
=
0
;
dpb
->
sector_bytes
=
sector_bytes
;
dpb
->
cluster_sectors
=
cluster_sectors
-
1
;
dpb
->
shift
=
0
;
while
(
cluster_sectors
>
1
)
{
INT21_HEAP
*
ptr
=
DOSVM_AllocDataUMB
(
sizeof
(
INT21_HEAP
),
&
heap_segment
,
&
heap_selector
);
INT21_FillHeap
(
ptr
);
heap_initialized
=
TRUE
;
cluster_sectors
/=
2
;
dpb
->
shift
++
;
}
if
(
!
ISV86
(
context
)
&&
DOSVM_IsWin16
())
return
heap_selector
;
else
return
heap_segment
;
dpb
->
num_reserved
=
0
;
dpb
->
num_FAT
=
1
;
dpb
->
num_root_entries
=
2
;
dpb
->
first_data_sector
=
2
;
dpb
->
num_clusters1
=
total_clusters
;
dpb
->
sectors_per_FAT
=
1
;
dpb
->
first_dir_sector
=
1
;
dpb
->
driver_header
=
0
;
dpb
->
media_ID
=
(
drivetype
==
DRIVE_FIXED
)
?
0xF8
:
0xF0
;
dpb
->
access_flag
=
0
;
dpb
->
next
=
0
;
dpb
->
search_cluster1
=
0
;
dpb
->
free_clusters_lo
=
LOWORD
(
free_clusters
);
dpb
->
free_clusters_hi
=
HIWORD
(
free_clusters
);
dpb
->
mirroring_flags
=
0
;
dpb
->
info_sector
=
0xffff
;
dpb
->
spare_boot_sector
=
0xffff
;
dpb
->
first_cluster_sector
=
0
;
dpb
->
num_clusters2
=
total_clusters
;
dpb
->
fat_clusters
=
32
;
dpb
->
root_cluster
=
0
;
dpb
->
search_cluster2
=
0
;
return
TRUE
;
}
...
...
@@ -2283,6 +2427,62 @@ static void INT21_Ioctl( CONTEXT86 *context )
/***********************************************************************
* INT21_Fat32
*
* Handler for function 0x73.
*/
static
BOOL
INT21_Fat32
(
CONTEXT86
*
context
)
{
switch
(
AL_reg
(
context
))
{
case
0x02
:
/* FAT32 - GET EXTENDED DPB */
{
BYTE
drive
=
INT21_MapDrive
(
DL_reg
(
context
)
);
WORD
*
ptr
=
CTX_SEG_OFF_TO_LIN
(
context
,
context
->
SegEs
,
context
->
Edi
);
INT21_DPB
*
target
=
(
INT21_DPB
*
)(
ptr
+
1
);
INT21_DPB
*
source
;
TRACE
(
"FAT32 - GET EXTENDED DPB %d
\n
"
,
DL_reg
(
context
)
);
if
(
CX_reg
(
context
)
<
sizeof
(
INT21_DPB
)
+
2
||
*
ptr
<
sizeof
(
INT21_DPB
)
)
{
SetLastError
(
ERROR_BAD_LENGTH
);
return
FALSE
;
}
if
(
!
INT21_FillDrivePB
(
drive
)
)
{
SetLastError
(
ERROR_INVALID_DRIVE
);
return
FALSE
;
}
source
=
&
INT21_GetHeapPointer
()
->
misc_dpb_list
[
drive
];
*
ptr
=
sizeof
(
INT21_DPB
);
memcpy
(
target
,
source
,
sizeof
(
INT21_DPB
));
if
(
LOWORD
(
context
->
Esi
)
!=
0xF1A6
)
{
target
->
driver_header
=
0
;
target
->
next
=
0
;
}
else
{
FIXME
(
"Caller requested driver and next DPB pointers!
\n
"
);
}
}
break
;
default:
INT_BARF
(
context
,
0x21
);
}
return
TRUE
;
}
/***********************************************************************
* INT21_LongFilename
*
* Handler for function 0x71.
...
...
@@ -2862,7 +3062,21 @@ void WINAPI DOSVM_Int21Handler( CONTEXT86 *context )
break
;
case
0x1f
:
/* GET DRIVE PARAMETER BLOCK FOR DEFAULT DRIVE */
INT_Int21Handler
(
context
);
{
BYTE
drive
=
INT21_MapDrive
(
0
);
TRACE
(
"GET DPB FOR DEFAULT DRIVE
\n
"
);
if
(
INT21_FillDrivePB
(
drive
))
{
SET_AL
(
context
,
0x00
);
/* success */
SET_BX
(
context
,
offsetof
(
INT21_HEAP
,
misc_dpb_list
[
drive
]
)
);
context
->
SegDs
=
INT21_GetHeapSelector
(
context
);
}
else
{
SET_AL
(
context
,
0xff
);
/* invalid or network drive */
}
}
break
;
case
0x20
:
/* NULL FUNCTION FOR CP/M COMPATIBILITY */
...
...
@@ -3008,7 +3222,21 @@ void WINAPI DOSVM_Int21Handler( CONTEXT86 *context )
break
;
case
0x32
:
/* GET DOS DRIVE PARAMETER BLOCK FOR SPECIFIC DRIVE */
INT_Int21Handler
(
context
);
{
BYTE
drive
=
INT21_MapDrive
(
DL_reg
(
context
)
);
TRACE
(
"GET DPB FOR SPECIFIC DRIVE %d
\n
"
,
DL_reg
(
context
)
);
if
(
INT21_FillDrivePB
(
drive
))
{
SET_AL
(
context
,
0x00
);
/* success */
SET_DX
(
context
,
offsetof
(
INT21_HEAP
,
misc_dpb_list
[
drive
]
)
);
context
->
SegDs
=
INT21_GetHeapSelector
(
context
);
}
else
{
SET_AL
(
context
,
0xff
);
/* invalid or network drive */
}
}
break
;
case
0x33
:
/* MULTIPLEXED */
...
...
@@ -3695,7 +3923,8 @@ void WINAPI DOSVM_Int21Handler( CONTEXT86 *context )
break
;
case
0x73
:
/* MSDOS7 - FAT32 */
INT_Int21Handler
(
context
);
if
(
!
INT21_Fat32
(
context
))
bSetDOSExtendedError
=
TRUE
;
break
;
case
0xdc
:
/* CONNECTION SERVICES - GET CONNECTION NUMBER */
...
...
msdos/int21.c
View file @
181a7cca
...
...
@@ -73,62 +73,9 @@ WINE_DEFAULT_DEBUG_CHANNEL(int21);
#define DOS_GET_DRIVE(reg) ((reg) ? (reg) - 1 : DRIVE_GetCurrentDrive())
/* Define the drive parameter block, as used by int21/1F
* and int21/32. This table can be accessed through the
* global 'dpb' pointer, which points into the local dos
* heap.
*/
struct
DPB
{
BYTE
drive_num
;
/* 0=A, etc. */
BYTE
unit_num
;
/* Drive's unit number (?) */
WORD
sector_size
;
/* Sector size in bytes */
BYTE
high_sector
;
/* Highest sector in a cluster */
BYTE
shift
;
/* Shift count (?) */
WORD
reserved
;
/* Number of reserved sectors at start */
BYTE
num_FAT
;
/* Number of FATs */
WORD
dir_entries
;
/* Number of root dir entries */
WORD
first_data
;
/* First data sector */
WORD
high_cluster
;
/* Highest cluster number */
WORD
sectors_in_FAT
;
/* Number of sectors per FAT */
WORD
start_dir
;
/* Starting sector of first dir */
DWORD
driver_head
;
/* Address of device driver header (?) */
BYTE
media_ID
;
/* Media ID */
BYTE
access_flag
;
/* Prev. accessed flag (0=yes,0xFF=no) */
DWORD
next
;
/* Pointer to next DPB in list */
WORD
free_search
;
/* Free cluster search start */
WORD
free_clusters
;
/* Number of free clusters (0xFFFF=unknown) */
};
struct
EDPB
/* FAT32 extended Drive Parameter Block */
{
/* from Ralf Brown's Interrupt List */
struct
DPB
dpb
;
/* first 24 bytes = original DPB */
BYTE
edpb_flags
;
/* undocumented/unknown flags */
DWORD
next_edpb
;
/* pointer to next EDPB */
WORD
free_cluster
;
/* cluster to start search for free space on write, typically
the last cluster allocated */
WORD
clusters_free
;
/* number of free clusters on drive or FFFF = unknown */
WORD
clusters_free_hi
;
/* hiword of clusters_free */
WORD
mirroring_flags
;
/* mirroring flags: bit 7 set = do not mirror active FAT */
/* bits 0-3 = 0-based number of the active FAT */
WORD
info_sector
;
/* sector number of file system info sector, or FFFF for none */
WORD
spare_boot_sector
;
/* sector number of backup boot sector, or FFFF for none */
DWORD
first_cluster
;
/* sector number of the first cluster */
DWORD
max_cluster
;
/* sector number of the last cluster */
DWORD
fat_clusters
;
/* number of clusters occupied by FAT */
DWORD
root_cluster
;
/* cluster number of start of root directory */
DWORD
free_cluster2
;
/* same as free_cluster: cluster at which to start
search for free space when writing */
};
DWORD
dpbsegptr
;
struct
DosHeap
{
BYTE
mediaID
;
BYTE
biosdate
[
8
];
struct
DPB
dpb
;
};
static
struct
DosHeap
*
heap
;
static
WORD
DosHeapHandle
;
...
...
@@ -143,7 +90,6 @@ static BOOL INT21_CreateHeap(void)
return
FALSE
;
}
heap
=
(
struct
DosHeap
*
)
GlobalLock16
(
DosHeapHandle
);
dpbsegptr
=
MAKESEGPTR
(
DosHeapHandle
,(
int
)
&
heap
->
dpb
-
(
int
)
heap
);
strcpy
(
heap
->
biosdate
,
"01/01/80"
);
return
TRUE
;
}
...
...
@@ -223,60 +169,6 @@ static int INT21_GetDriveAllocInfo( CONTEXT86 *context )
return
1
;
}
static
int
FillInDrivePB
(
int
drive
)
{
if
(
!
DRIVE_IsValid
(
drive
))
{
SetLastError
(
ERROR_INVALID_DRIVE
);
return
0
;
}
else
if
(
heap
||
INT21_CreateHeap
())
{
/* FIXME: I have no idea what a lot of this information should
* say or whether it even really matters since we're not allowing
* direct block access. However, some programs seem to depend on
* getting at least _something_ back from here. The 'next' pointer
* does worry me, though. Should we have a complete table of
* separate DPBs per drive? Probably, but I'm lazy. :-) -CH
*/
heap
->
dpb
.
drive_num
=
heap
->
dpb
.
unit_num
=
drive
;
/*The same?*/
heap
->
dpb
.
sector_size
=
512
;
heap
->
dpb
.
high_sector
=
1
;
heap
->
dpb
.
shift
=
drive
<
2
?
0
:
6
;
/*6 for HD, 0 for floppy*/
heap
->
dpb
.
reserved
=
0
;
heap
->
dpb
.
num_FAT
=
1
;
heap
->
dpb
.
dir_entries
=
2
;
heap
->
dpb
.
first_data
=
2
;
heap
->
dpb
.
high_cluster
=
64000
;
heap
->
dpb
.
sectors_in_FAT
=
1
;
heap
->
dpb
.
start_dir
=
1
;
heap
->
dpb
.
driver_head
=
0
;
heap
->
dpb
.
media_ID
=
(
drive
>
1
)
?
0xF8
:
0xF0
;
heap
->
dpb
.
access_flag
=
0
;
heap
->
dpb
.
next
=
0
;
heap
->
dpb
.
free_search
=
0
;
heap
->
dpb
.
free_clusters
=
0xFFFF
;
/* unknown */
return
1
;
}
return
0
;
}
static
void
GetDrivePB
(
CONTEXT86
*
context
,
int
drive
)
{
if
(
FillInDrivePB
(
drive
))
{
SET_AL
(
context
,
0x00
);
context
->
SegDs
=
SELECTOROF
(
dpbsegptr
);
SET_BX
(
context
,
OFFSETOF
(
dpbsegptr
)
);
}
else
{
SET_AX
(
context
,
0x00ff
);
}
}
static
BOOL
ioctlGenericBlkDevReq
(
CONTEXT86
*
context
)
{
BYTE
*
dataptr
=
CTX_SEG_OFF_TO_LIN
(
context
,
context
->
SegDs
,
context
->
Edx
);
...
...
@@ -722,20 +614,10 @@ void WINAPI INT_Int21Handler( CONTEXT86 *context )
if
(
!
INT21_GetDriveAllocInfo
(
context
))
SET_AX
(
context
,
0xffff
);
break
;
case
0x1f
:
/* GET DRIVE PARAMETER BLOCK FOR DEFAULT DRIVE */
GetDrivePB
(
context
,
DRIVE_GetCurrentDrive
());
break
;
case
0x29
:
/* PARSE FILENAME INTO FCB */
INT21_ParseFileNameIntoFCB
(
context
);
break
;
case
0x32
:
/* GET DOS DRIVE PARAMETER BLOCK FOR SPECIFIC DRIVE */
TRACE
(
"GET DOS DRIVE PARAMETER BLOCK FOR DRIVE %s
\n
"
,
INT21_DriveName
(
DL_reg
(
context
)));
GetDrivePB
(
context
,
DOS_GET_DRIVE
(
DL_reg
(
context
)
)
);
break
;
case
0x36
:
/* GET FREE DISK SPACE */
TRACE
(
"GET FREE DISK SPACE FOR DRIVE %s
\n
"
,
INT21_DriveName
(
DL_reg
(
context
)));
...
...
@@ -941,86 +823,6 @@ void WINAPI INT_Int21Handler( CONTEXT86 *context )
}
break
;
case
0x73
:
/* MULTIPLEXED: Win95 OSR2/Win98 FAT32 calls */
TRACE
(
"windows95 function AX %04x
\n
"
,
AX_reg
(
context
));
switch
(
AL_reg
(
context
))
{
case
0x02
:
/* Get Extended Drive Parameter Block for specific drive */
/* ES:DI points to word with length of data (should be 0x3d) */
{
WORD
*
buffer
;
struct
EDPB
*
edpb
;
DWORD
cluster_sectors
,
sector_bytes
,
free_clusters
,
total_clusters
;
char
root
[]
=
"A:
\\
"
;
buffer
=
(
WORD
*
)
CTX_SEG_OFF_TO_LIN
(
context
,
context
->
SegEs
,
context
->
Edi
);
TRACE
(
"Get Extended DPB: linear buffer address is %p
\n
"
,
buffer
);
/* validate passed-in buffer lengths */
if
((
*
buffer
!=
0x3d
)
||
(
context
->
Ecx
!=
0x3f
))
{
WARN
(
"Get Extended DPB: buffer lengths incorrect
\n
"
);
WARN
(
"CX = %lx, buffer[0] = %x
\n
"
,
context
->
Ecx
,
*
buffer
);
SET_CFLAG
(
context
);
SET_AL
(
context
,
0x18
);
/* bad buffer length */
}
/* buffer checks out */
buffer
++
;
/* skip over length word now */
if
(
FillInDrivePB
(
DX_reg
(
context
)
)
)
{
edpb
=
(
struct
EDPB
*
)
buffer
;
/* copy down the old-style DPB portion first */
memcpy
(
&
edpb
->
dpb
,
&
heap
->
dpb
,
sizeof
(
struct
DPB
));
/* now fill in the extended entries */
edpb
->
edpb_flags
=
0
;
edpb
->
next_edpb
=
0
;
edpb
->
free_cluster
=
edpb
->
free_cluster2
=
0
;
/* determine free disk space */
*
root
+=
DOS_GET_DRIVE
(
DX_reg
(
context
)
);
GetDiskFreeSpaceA
(
root
,
&
cluster_sectors
,
&
sector_bytes
,
&
free_clusters
,
&
total_clusters
);
edpb
->
clusters_free
=
(
free_clusters
&
0xffff
);
edpb
->
clusters_free_hi
=
free_clusters
>>
16
;
edpb
->
mirroring_flags
=
0
;
edpb
->
info_sector
=
0xffff
;
edpb
->
spare_boot_sector
=
0xffff
;
edpb
->
first_cluster
=
0
;
edpb
->
max_cluster
=
total_clusters
;
edpb
->
fat_clusters
=
32
;
/* made-up value */
edpb
->
root_cluster
=
0
;
RESET_CFLAG
(
context
);
/* clear carry */
SET_AX
(
context
,
0
);
}
else
{
SET_AX
(
context
,
0x00ff
);
SET_CFLAG
(
context
);
}
}
break
;
case
0x03
:
/* Get Extended free space on drive */
case
0x04
:
/* Set DPB for formatting */
case
0x05
:
/* extended absolute disk read/write */
FIXME
(
"Unimplemented FAT32 int32 function %04x
\n
"
,
AX_reg
(
context
));
SET_CFLAG
(
context
);
SET_AL
(
context
,
0
);
break
;
}
break
;
default:
INT_BARF
(
context
,
0x21
);
break
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment