const std = @import("std"); const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; const print = std.debug.print; const c = @cImport({ @cInclude("errno.h"); @cInclude("stdio.h"); if (@import("builtin").os.tag != .windows) { @cInclude("sys/statvfs.h"); } }); const paths = @import("../paths.zig"); const formatDosPath = paths.formatDosPath; const convertTo83 = paths.convertTo83; const types = @import("./lib/types.zig"); const CommandStatus = types.CommandStatus; const CommandContext = types.CommandContext; const OutputCapture = types.OutputCapture; const InputSource = types.InputSource; fn formatDosDateTime(allocator: Allocator, timestamp_secs: i64) ![]const u8 { const epoch_seconds = @as(u64, @intCast(@max(timestamp_secs, 0))); const epoch_day = @divFloor(epoch_seconds, std.time.s_per_day); const day_seconds = epoch_seconds % std.time.s_per_day; // Calculate date (simplified) var year: u32 = 1970; // Start from Unix epoch year var remaining_days = epoch_day; // Simple year calculation while (remaining_days >= 365) { const days_in_year: u64 = if (isLeapYear(year)) 366 else 365; if (remaining_days < days_in_year) break; remaining_days -= days_in_year; year += 1; } // Simple month/day calculation (approximate) const month = @min(@divFloor(remaining_days, 30) + 1, 12); const day = @min(remaining_days % 30 + 1, 31); // Calculate time const hours = day_seconds / std.time.s_per_hour; const minutes = (day_seconds % std.time.s_per_hour) / std.time.s_per_min; // Format as MM-DD-YY HH:MMa (DOS style) const am_pm = if (hours < 12) "a" else "p"; const display_hour = if (hours == 0) 12 else if (hours > 12) hours - 12 else hours; return try std.fmt.allocPrint(allocator, "{d:0>2}-{d:0>2}-{d:0>2} {d:>2}:{d:0>2}{s}", .{ @as(u32, @intCast(month)), @as(u32, @intCast(day)), @as(u32, @intCast(year % 100)), @as(u32, @intCast(display_hour)), @as(u32, @intCast(minutes)), am_pm }); } fn formatWithCommas(allocator: Allocator, number: u64) ![]const u8 { // Convert number to string first const num_str = try std.fmt.allocPrint(allocator, "{d}", .{number}); defer allocator.free(num_str); // Calculate how many commas we need const digits = num_str.len; const comma_count = if (digits <= 3) 0 else @divFloor(digits - 1, 3); if (comma_count == 0) { return try allocator.dupe(u8, num_str); } // Allocate space for number + commas const result = try allocator.alloc(u8, digits + comma_count); var result_idx: usize = result.len; var num_idx: usize = num_str.len; var digit_count: usize = 0; // Work backwards, adding commas every 3 digits while (num_idx > 0) { num_idx -= 1; result_idx -= 1; result[result_idx] = num_str[num_idx]; digit_count += 1; // Add comma every 3 digits (but not at the beginning) if (digit_count % 3 == 0 and num_idx > 0) { result_idx -= 1; result[result_idx] = ','; } } return result; } fn isLeapYear(year: u32) bool { return (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0); } const GetFreeDiskSpaceError = error{ NotImplemented, AccessDenied }; fn getFreeDiskSpace(path: []const u8) GetFreeDiskSpaceError!u64 { if (@import("builtin").os.tag == .windows) { return error.NotImplemented; } var stat: c.struct_statvfs = undefined; if (c.statvfs(path.ptr, &stat) != 0) { _ = c.perror("statvfs"); return error.AccessDenied; } return stat.f_bsize * stat.f_bfree; } pub const Dir = struct { path: []const u8, pub fn eval(dir: Dir, ctx: CommandContext) !CommandStatus { var output_buffer = ArrayList(u8).init(ctx.allocator); defer output_buffer.deinit(); // Format path in DOS style with backslashes and uppercase drive letter const formatted_path = try formatDosPath(ctx.allocator, dir.path); defer ctx.allocator.free(formatted_path); // Get volume label (simplified - just show drive) const drive_letter = if (formatted_path.len >= 2 and formatted_path[1] == ':') formatted_path[0] else 'C'; try output_buffer.writer().print(" Volume in drive {c} has no label\n", .{drive_letter}); try output_buffer.writer().print(" Volume Serial Number is 1234-5678\n", .{}); try output_buffer.writer().print("\n Directory of {s}\n\n", .{formatted_path}); var dir_iterator = std.fs.cwd().openDir(dir.path, .{ .iterate = true }) catch { const error_msg = "File not found\n"; if (ctx.output_capture) |capture| { try capture.write(error_msg); } else { print("{s}", .{error_msg}); } return CommandStatus{ .Code = 1 }; }; defer dir_iterator.close(); var iterator = dir_iterator.iterate(); var file_count: u32 = 0; var dir_count: u32 = 0; var total_file_bytes: u64 = 0; while (try iterator.next()) |entry| { const stat = dir_iterator.statFile(entry.name) catch continue; // Convert timestamp to DOS date/time format const mtime_secs = @divFloor(stat.mtime, std.time.ns_per_s); const date_time = try formatDosDateTime(ctx.allocator, @intCast(mtime_secs)); defer ctx.allocator.free(date_time); // Convert filename to 8.3 format const short_name = try convertTo83(ctx.allocator, entry.name); defer ctx.allocator.free(short_name.name); defer ctx.allocator.free(short_name.ext); // Create lowercase versions of name and extension const lower_name_buf = try ctx.allocator.alloc(u8, short_name.name.len); defer ctx.allocator.free(lower_name_buf); const lower_name = std.ascii.lowerString(lower_name_buf, short_name.name); const lower_ext_buf = try ctx.allocator.alloc(u8, short_name.ext.len); defer ctx.allocator.free(lower_ext_buf); const lower_ext = std.ascii.lowerString(lower_ext_buf, short_name.ext); switch (entry.kind) { .directory => { try output_buffer.writer().print("{s:<8} {s:<3} {s}\n", .{ lower_name, lower_ext, date_time }); dir_count += 1; }, .file => { const formatted_size = try formatWithCommas(ctx.allocator, stat.size); defer ctx.allocator.free(formatted_size); try output_buffer.writer().print("{s:<8} {s:<3} {s:>14} {s}\n", .{ lower_name, lower_ext, formatted_size, date_time }); file_count += 1; total_file_bytes += stat.size; }, else => {}, } } // Get free disk space using statvfs const path = try std.fs.cwd().realpathAlloc(ctx.allocator, dir.path); defer ctx.allocator.free(path); const bytes_free = getFreeDiskSpace(path) catch |err| switch (err) { error.AccessDenied => 0, error.NotImplemented => 0, }; const formatted_total_bytes = try formatWithCommas(ctx.allocator, total_file_bytes); defer ctx.allocator.free(formatted_total_bytes); const formatted_free_bytes = try formatWithCommas(ctx.allocator, bytes_free); defer ctx.allocator.free(formatted_free_bytes); try output_buffer.writer().print(" {d} File(s) {s:>14} bytes\n", .{ file_count, formatted_total_bytes }); try output_buffer.writer().print(" {d} Dir(s) {s:>14} bytes free\n", .{ dir_count, formatted_free_bytes }); if (ctx.output_capture) |capture| { try capture.write(output_buffer.items); } else { print("{s}", .{output_buffer.items}); } return CommandStatus{ .Code = 0 }; } };