나래온 툴 내부 구조 – 2. 경로가 지정된 객체들

필요성

나래온 툴에는 경로가 필요한 객체들이 많다. 대표적으로 물리 드라이브 그 자체인 TPhysicalDrive부터, 수많은 Getter 객체들이 모두 해당 드라이브의 경로를 필요로 한다. 이외에도 파티션을 지정하는 객체의 경우에도 경로를 필요로 한다. 이 경우 각 행동을 수행할 때마다 경로를 기입하게 하는 것은 부자연스러울 뿐더러 혼동의 가능성이 매우 크다. 따라서 객체가 생성될 때 경로를 지정하고 내부에서 경로를 보관하게 하는 식으로 설계하였다.

따라서 본편의 내용은 자칫 불필요하게 보일 수 있지만, 본편의 내용은 프로젝트 전체에 걸쳐 반복되므로 필수적으로 이해가 필요하다.

생각보다 내용이 길어져 두 편으로 분리하게 되었다. PhysicalDrive 관련한 내용은 다음 편에 나올 예정이다.

기본형: TOSFile

가장 기본적인 형태는 TOSFile이다. (어떤 경로던 확인하지 않고) 경로를 저장하며, OS 에러 코드를 exception으로 바꾸는 기능을 탑재하고 있다. GetLastError를 체크해서 매번 에러 처리를 하는 일은 매우 복잡해 잊기 쉽다. 하지만 이 구조에서는 IfOSErrorRaiseException 함수를 부르는 것으로 간단하게 이 문제를 해결할 수 있다. 해당 부분의 소스는 다음과 같다.

function TOSFile.IsLastSystemCallSucceed: Boolean;
begin
  result :=
    GetLastError = ERROR_SUCCESS;
end;

function TOSFile.GetOSErrorString(const OSErrorCode: Integer): String;
begin
  result :=
    'OS Error: ' +
      SysErrorMessage(OSErrorCode) + ' (' + IntToStr(OSErrorCode) + ')';
end;

procedure TOSFile.IfOSErrorRaiseException;
var
  OSErrorException: EOSError;
begin
  if not IsLastSystemCallSucceed then
  begin
    OSErrorException := EOSError.Create(GetOSErrorString(GetLastError));
    OSErrorException.ErrorCode := GetLastError;
    raise OSErrorException;
  end;
end;

또한 내부에서 빈번하게 이뤄지는 요청 중 하나가 접두어를 분리하는 문제다. 예를 들어, \\.\PhysicalDrive7에서 7을 뽑아내는 일, \\.\C:에서 C:를 뽑아내는 일이다. 이 부분을 GetPathOfFileAccessingWithoutPrefix 함수로 구현하였다.

function TOSFile.DeletePrefix(const PrefixToDelete: String): String;
var
  PathToDeletePrefix: String;
begin
  PathToDeletePrefix := GetPathOfFileAccessing;
  result :=
    Copy(PathToDeletePrefix, Length(PrefixToDelete) + 1,
      Length(PathToDeletePrefix) - Length(PrefixToDelete));
end;

function TOSFile.IsPathOfFileAccessingHavePrefix(
  const PrefixToCheck: String): Boolean;
begin
  result :=
    Copy(GetPathOfFileAccessing, 0, Length(PrefixToCheck)) = PrefixToCheck;
end;

function TOSFile.GetPathOfFileAccessingWithoutPrefix: String;
begin
  if IsPathOfFileAccessingHavePrefix(
    ThisComputerPrefix + PhysicalDrivePrefix) then
      exit(DeletePrefix(ThisComputerPrefix + PhysicalDrivePrefix))
  else if IsPathOfFileAccessingHavePrefix(ThisComputerPrefix) then
    exit(DeletePrefix(ThisComputerPrefix))
  else
    exit(GetPathOfFileAccessing);
end;

윈도우 경로는 대소문자를 구별하지 않기 때문에, 내부에서는 대문자로 통일된다. 문자열 비교도 따로 IsPathEqual 함수를 제공하여 오류가 없게 했다.

function TOSFile.IsPathEqual(const PathToCompare: String): Boolean;
begin
  result := GetPathOfFileAccessing = UpperCase(PathToCompare);
end;

constructor TOSFile.Create(const FileToGetAccess: String);
begin
  inherited Create;
  PathOfFileAccessing := UpperCase(FileToGetAccess);
end;

핸들이 필요할 때: TOSFileWithHandle

경로를 직접 사용해 API를 호출할 때가 아니라, 핸들이 필요할 때가 있다. 이런 때 사용하는 것이 TOSFileWithHandle 객체다. 이 클래스 내부의 GetMinimumPrivilege 함수를 override하여 핸들들이 각 상황에 알맞는 권한(읽기, 쓰기 권한)만을 가지게 된다. 또한 설정이 매우 복잡한 Security descriptor를 생성하는 과정 또한 통합해 핸들의 보안성을 높였다.

procedure TOSFileWithHandle.CreateHandle(const FileToGetAccess: String;
  const DesiredAccess: TCreateFileDesiredAccess);
begin
  if FileHandle <> nil then
    raise EInvalidOp.Create('Invalid Operation: Don''t create handle twice');
  inherited Create(FileToGetAccess);
  FileHandle := TFileHandle.Create(FileToGetAccess, DesiredAccess);
end;

대부분의 작업들은 TFileHandle 내부에서 이루어지는데, 여기에 있는 Unlock 함수는 좀 특별하다. 이 함수는 이동식 저장장치면서 SAT 장치인 스토리지(Z80)에 초기화 ISO 기록 등을 위해 직접 접근해야 할 때, 프로그램에 남아있던 모든 핸들을 해제하기 위해서 사용된다. 해당 작업이 완료되는 즉시 핸들을 새로 만들어야 하므로, 상태 자체가 IOSFileUnlock 인터페이스로 관리된다.

constructor TOSFileUnlock.Create(const Handle: PTHandle; const Path: String;
  const AccessPrevilege: TCreateFileDesiredAccess);
begin
  inherited Create();
  FileHandle := Handle;
  if IsHandleValid(FileHandle^) then
    CloseHandle(FileHandle^);
  self.Path := Path;
  self.AccessPrivilege := AccessPrevilege;
end;

destructor TOSFileUnlock.Destroy;
begin
  FileHandle^ := CreateHandle(Path, AccessPrivilege);
  inherited;
end;

ioctl이 필요할 때: TIoControlFile

ioctl은 그 자체로 프로그램 내에서 상당히 빈번히 쓰이는 패턴이다. 따라서 별도의 객체로 분리해버리고, ioctl code를 따로 분리하게 만들었다. 하지만 가장 큰 의의는 메타클래스를 통해 ioctl에 들어갈 구조체의 형을 명확하게 지정할 수 있다는 점이다. 또한 그를 통해 버퍼 사이즈 지정 부분에 오류가 없게 된다. 이는 반복적인 패턴에서 빈번하게 발생하는 오류라서 연구를 통해 집어넣게 되었다.

function TIoControlFile.BuildOSBufferBy<InputType, OutputType>(
  var InputBuffer: InputType; var OutputBuffer: OutputType): TIoControlIOBuffer;
begin
  result.InputBuffer.Size := SizeOf(InputBuffer);
  result.InputBuffer.Buffer := @InputBuffer;
  result.OutputBuffer.Size := SizeOf(OutputBuffer);
  result.OutputBuffer.Buffer := @OutputBuffer;
end;

function TIoControlFile.TDeviceIoControlCodeToOSControlCode(
  ControlCode: TIoControlCode): Integer;
const
  IOCTL_SCSI_BASE = FILE_DEVICE_CONTROLLER;
  IOCTL_STORAGE_BASE = $2D;
  IOCTL_ATA_PASS_THROUGH =
    (IOCTL_SCSI_BASE shl 16) or
    ((FILE_READ_ACCESS or FILE_WRITE_ACCESS) shl 14) or ($040B shl 2) or
    (METHOD_BUFFERED);
  IOCTL_ATA_PASS_THROUGH_DIRECT = $4D030;
  IOCTL_SCSI_PASS_THROUGH =
    (IOCTL_SCSI_BASE shl 16) or
    ((FILE_READ_ACCESS or FILE_WRITE_ACCESS) shl 14) or ($0401 shl 2) or
    (METHOD_BUFFERED);
  IOCTL_STORAGE_PROTOCOL_COMMAND =
    (IOCTL_SCSI_BASE shl 16) or
    ((FILE_READ_ACCESS or FILE_WRITE_ACCESS) shl 14) or ($04F0 shl 2) or
    (METHOD_BUFFERED);
  IOCTL_SCSI_MINIPORT =
    (IOCTL_SCSI_BASE shl 16) or
    ((FILE_READ_ACCESS or FILE_WRITE_ACCESS) shl 14) or ($0402 shl 2) or
    (METHOD_BUFFERED);
  IOCTL_STORAGE_MANAGE_DATA_SET_ATTRIBUTES =
    (IOCTL_STORAGE_BASE shl 16) or
    (FILE_WRITE_ACCESS shl 14) or ($0501 shl 2) or
    (METHOD_BUFFERED);
  IOCTL_SCSI_GET_ADDRESS =
    (IOCTL_SCSI_BASE shl 16) or
    (FILE_ANY_ACCESS shl 14) or ($0406 shl 2) or
    (METHOD_BUFFERED);

  OSControlCodeOfIoControlCode: Array[TIoControlCode] of Integer =
    (IOCTL_ATA_PASS_THROUGH,
     IOCTL_ATA_PASS_THROUGH_DIRECT,
     IOCTL_SCSI_PASS_THROUGH,
     IOCTL_STORAGE_PROTOCOL_COMMAND,
     IOCTL_STORAGE_QUERY_PROPERTY,
     IOCTL_STORAGE_CHECK_VERIFY,
     FSCTL_GET_VOLUME_BITMAP,
     IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,
     FSCTL_GET_NTFS_VOLUME_DATA,
     IOCTL_DISK_GET_DRIVE_GEOMETRY_EX,
     IOCTL_STORAGE_MANAGE_DATA_SET_ATTRIBUTES,
     IOCTL_SCSI_MINIPORT,
     IOCTL_SCSI_GET_ADDRESS,
     FSCTL_LOCK_VOLUME,
     FSCTL_UNLOCK_VOLUME,
     IOCTL_STORAGE_LOAD_MEDIA,
     0);
begin
  if (ControlCode = TIoControlCode.Unknown) or (IsOutOfRange(ControlCode)) then
    raise EInvalidIoControlCode.Create
      ('InvalidIoControlCode: There''s no such IoControlCode - ' +
       IntToStr(Cardinal(ControlCode)));
  exit(OSControlCodeOfIoControlCode[ControlCode]);
end;

요약

나래온 툴의 객체들은 경로를 필요로 하고, 반복되는 부분을 다음과 같은 클래스로 만들었다.

  1. TOSFile: Path
  2. TOSFileWithHandle: Path + Handle
  3. TIoControlFile: Path + Handle + Ioctl

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다