Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: CodeQL

# Static application security testing (SAST). CodeQL builds the Swift package,
# analyzes it with the security-extended query suite, and uploads results to the
# repo's Security ▸ Code scanning tab. Swift analysis requires a macOS runner.

on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '23 5 * * 1' # weekly, Mondays 05:23 UTC — catches new queries on unchanged code

concurrency:
group: codeql-${{ github.ref }}
cancel-in-progress: true

jobs:
analyze:
name: Analyze (Swift)
runs-on: macos-15
permissions:
security-events: write # upload SARIF to code scanning
actions: read
contents: read
steps:
- uses: actions/checkout@v5

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: swift
build-mode: autobuild
queries: security-extended

- name: Autobuild
uses: github/codeql-action/autobuild@v3

- name: Analyze
uses: github/codeql-action/analyze@v3
with:
category: "/language:swift"
4 changes: 2 additions & 2 deletions App/Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.9.7</string>
<string>0.9.7.1</string>
<key>CFBundleVersion</key>
<string>0.9.7</string>
<string>0.9.7.1</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSMinimumSystemVersion</key>
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to yafm are recorded here.
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
the project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.9.7.1] — Hotfix: disk-image volume classification

### Fixed
- **Mounted disk image misclassified as External SSD** — a mounted `.dmg`/`.sparsebundle`
is a local ejectable APFS volume, so volume detection tagged it "External SSD". It is now
recognized via DiskArbitration (`DADeviceProtocol`/`DADeviceModel` = "Disk Image") and
shown as a Virtual volume.

## [0.9.7] — Calm pass: function bar · Settings polish · file-engine fixes

### Fixed
Expand Down
23 changes: 17 additions & 6 deletions Core/Sources/Core/VolumeDetection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,27 +79,32 @@ public struct VolumeInfoCollector: Sendable {
public init() {}

public func collect(volume: Volume) -> VolumeClassification {
let (filesystem, bsdName) = daInfo(mountURL: volume.url)
let (filesystem, bsdName, isDiskImage) = daInfo(mountURL: volume.url)
let transport = resolveTransport(bsdName: bsdName, filesystem: filesystem, volume: volume)
let isReadOnly = readOnly(mountURL: volume.url)
let cameraFolders = probeCameraFolders(mountURL: volume.url)
let hasTM = probeTimeMachine(mountURL: volume.url)
return VolumeClassifier.classify(
volume: volume, filesystem: filesystem, transport: transport,
isReadOnly: isReadOnly, cameraFolders: cameraFolders, hasTimeMachine: hasTM
isReadOnly: isReadOnly, cameraFolders: cameraFolders, hasTimeMachine: hasTM,
isDiskImage: isDiskImage
)
}

// MARK: DiskArbitration

private func daInfo(mountURL: URL) -> (filesystem: String?, bsdName: String?) {
private func daInfo(mountURL: URL) -> (filesystem: String?, bsdName: String?, isDiskImage: Bool) {
guard let session = DASessionCreate(kCFAllocatorDefault),
let disk = DADiskCreateFromVolumePath(kCFAllocatorDefault, session, mountURL as CFURL),
let desc = DADiskCopyDescription(disk) else { return (nil, nil) }
let desc = DADiskCopyDescription(disk) else { return (nil, nil, false) }
let d = desc as NSDictionary
let fs = d[kDADiskDescriptionVolumeKindKey] as? String
let bsd = d[kDADiskDescriptionMediaBSDNameKey] as? String
return (fs, bsd)
// A mounted disk image (.dmg/.sparsebundle) reports protocol/model "Disk Image".
let proto = (d[kDADiskDescriptionDeviceProtocolKey] as? String)?.lowercased() ?? ""
let model = (d[kDADiskDescriptionDeviceModelKey] as? String)?.lowercased() ?? ""
let isDiskImage = proto.contains("disk image") || model.contains("disk image")
return (fs, bsd, isDiskImage)
}

// MARK: Transport
Expand Down Expand Up @@ -164,7 +169,8 @@ public struct VolumeInfoCollector: Sendable {
public struct VolumeClassifier: Sendable {
public static func classify(
volume: Volume, filesystem: String?, transport: TransportType,
isReadOnly: Bool, cameraFolders: [String], hasTimeMachine: Bool
isReadOnly: Bool, cameraFolders: [String], hasTimeMachine: Bool,
isDiskImage: Bool = false
) -> VolumeClassification {
func result(_ kind: ExternalVolumeKind, _ confidence: Double, _ reasons: [String]) -> VolumeClassification {
VolumeClassification(kind: kind, confidence: confidence, filesystem: filesystem,
Expand All @@ -181,6 +187,11 @@ public struct VolumeClassifier: Sendable {
return result(.networkVolume, 1.0, ["Network volume"])
}

// Mounted disk image (.dmg/.sparsebundle): virtual, not real external media.
if isDiskImage {
return result(.virtualVolume, 0.95, ["Mounted disk image"])
}

let fs = filesystem?.lowercased() ?? ""

// Camera card: folder indicators dominate
Expand Down
51 changes: 51 additions & 0 deletions Core/Tests/CoreTests/VolumeClassifierTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import XCTest
@testable import Core

final class VolumeClassifierTests: XCTestCase {

private func volume(
internal isInternal: Bool = false, local: Bool = true,
removable: Bool = false, ejectable: Bool = false,
capacity: Int64? = nil
) -> Volume {
Volume(
url: URL(fileURLWithPath: "/Volumes/Test"), name: "Test",
isRemovable: removable, isEjectable: ejectable,
isInternal: isInternal, isLocal: local,
totalCapacity: capacity, availableCapacity: capacity
)
}

private func classify(_ v: Volume, fs: String?, transport: TransportType = .unknown,
camera: [String] = [], tm: Bool = false,
diskImage: Bool = false) -> VolumeClassification {
VolumeClassifier.classify(
volume: v, filesystem: fs, transport: transport,
isReadOnly: false, cameraFolders: camera, hasTimeMachine: tm,
isDiskImage: diskImage
)
}

// Regression: a mounted .dmg is a local ejectable APFS volume; without the
// disk-image flag it was misclassified as an External SSD.
func testMountedDiskImageIsVirtual() {
let v = volume(ejectable: true, capacity: 100_000_000)
let c = classify(v, fs: "apfs", diskImage: true)
XCTAssertEqual(c.kind, .virtualVolume)
XCTAssertEqual(c.kind.label, "Virtual")
}

// Same APFS ejectable volume, but NOT a disk image -> real external SSD.
func testEjectableAPFSIsExternalSSD() {
let v = volume(ejectable: true)
let c = classify(v, fs: "apfs", diskImage: false)
XCTAssertEqual(c.kind, .externalSSD)
}

// Internal wins over everything, even if the disk-image flag is set.
func testInternalDominatesDiskImage() {
let v = volume(internal: true)
let c = classify(v, fs: "apfs", diskImage: true)
XCTAssertEqual(c.kind, .internalDisk)
}
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

**Yet Another File Manager for macOS.** A fast, keyboard-driven, modern alternative to Finder — that never freezes silently.

> Status: **v0.9.7 — 1.0 candidate.** Native Swift / SwiftUI. The spine (v0.1–0.3:
> Status: **v0.9.7.1 — 1.0 candidate.** Native Swift / SwiftUI. The spine (v0.1–0.3:
> dual-pane + tabs, async listing, own file engine, tags, JS plugins, git, search) is
> done; v0.4–0.9 added the keyboard-first speed layer, content search, SMB, the plugin
> capability model, archives, accessibility, and a Russian UI — then a five-dimension
Expand Down
2 changes: 1 addition & 1 deletion VISION.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,6 @@ The arc in brief:
tiles + sidebar drag-reorder + app handling · collapsible sidebar + in-window Settings ·
**calm UI pass + file-engine fixes**: redesigned function bar, Settings polish, tiles off by
default, cross-volume move, runaway-plugin watchdog, SMB timeout, notarization entitlements).
Currently **v0.9.7**; before 1.0: notarized
Currently **v0.9.7.1**; before 1.0: notarized
DMG (paid cert) + final polish. Photo-ingest + marketplace deferred post-1.0. Full
status-tracked breakdown in [`ROADMAP.md`](ROADMAP.md).
Loading