starnix_task_command/
lib.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#![warn(missing_docs)]
6
7//! The `TaskCommand` type and associated functions.
8
9use flyweights::FlyByteStr;
10use std::ops::Range;
11
12/// The command for a task.
13///
14/// Linux task commands are limited to 15 bytes, but Fuchsia allows longer names in places. It's
15/// useful to store longer names diagnostics and debugging information.
16#[derive(Clone, Eq, Hash, PartialEq)]
17pub struct TaskCommand {
18    name: FlyByteStr,
19    linux_name_range: Option<Range<usize>>,
20}
21
22impl TaskCommand {
23    /// Create a new `TaskCommand` from a byte slice. The byte slice is truncated at the first null
24    /// byte if any.
25    pub fn new(name: &[u8]) -> Self {
26        let name = if let Some(idx) = memchr::memchr(b'\0', name) { &name[..idx] } else { name };
27        Self { name: FlyByteStr::new(name), linux_name_range: None }
28    }
29
30    /// Create a new `TaskCommand` from a path. The basename of the path is used as the name.
31    pub fn from_path_bytes(path: &[u8]) -> Self {
32        let basename =
33            if let Some(idx) = memchr::memrchr(b'/', path) { &path[idx + 1..] } else { path };
34        Self::new(basename)
35    }
36
37    /// Returns the name truncated to 15 bytes.
38    pub fn comm_name(&self) -> &[u8] {
39        let bytes = self.linux_name_bytes();
40        &bytes[..std::cmp::min(bytes.len(), 15)]
41    }
42
43    /// Returns the name as a 16-byte array, null-terminated if shorter than 16 bytes,
44    /// as expected by `prctl(PR_GET_NAME)`.
45    pub fn prctl_name(&self) -> [u8; 16] {
46        let mut name = [0u8; 16];
47        let comm = self.comm_name();
48        name[..comm.len()].copy_from_slice(comm);
49        name
50    }
51
52    /// Returns the entire name as a byte slice.
53    pub fn as_bytes(&self) -> &[u8] {
54        self.name.as_bytes()
55    }
56
57    /// Returns the Linux name as a byte slice, without truncation.
58    fn linux_name_bytes(&self) -> &[u8] {
59        if let Some(range) = &self.linux_name_range {
60            &self.name.as_bytes()[range.clone()]
61        } else {
62            self.name.as_bytes()
63        }
64    }
65
66    /// Tries to embed `other` as the Linux name within this command.
67    /// Returns a new `TaskCommand` if `other` is a substring of this command.
68    pub fn try_embed(&self, other: &TaskCommand) -> Option<Self> {
69        use bstr::ByteSlice;
70        self.name.as_bytes().find(other.linux_name_bytes()).map(|offset| Self {
71            name: self.name.clone(),
72            linux_name_range: Some(offset..offset + other.linux_name_bytes().len()),
73        })
74    }
75}
76
77impl Default for TaskCommand {
78    fn default() -> Self {
79        Self::new(b"")
80    }
81}
82
83impl std::fmt::Debug for TaskCommand {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        self.name.fmt(f)
86    }
87}
88
89impl std::fmt::Display for TaskCommand {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        self.name.fmt(f)
92    }
93}
94
95impl PartialOrd for TaskCommand {
96    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
97        Some(self.cmp(other))
98    }
99}
100
101impl Ord for TaskCommand {
102    /// This comparison ignores the linux rendering of the name and provides a total ordering
103    /// based on the full name.
104    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
105        self.name.cmp(&other.name)
106    }
107}
108
109impl Into<FlyByteStr> for TaskCommand {
110    fn into(self) -> FlyByteStr {
111        self.name
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_new() {
121        assert_eq!(TaskCommand::new(b"foo").as_bytes(), b"foo");
122        assert_eq!(TaskCommand::new(b"foo\0bar").as_bytes(), b"foo");
123    }
124
125    #[test]
126    fn test_from_path_bytes() {
127        assert_eq!(TaskCommand::from_path_bytes(b"/foo/bar").as_bytes(), b"bar");
128        assert_eq!(TaskCommand::from_path_bytes(b"bar").as_bytes(), b"bar");
129        assert_eq!(TaskCommand::from_path_bytes(b"/bar").as_bytes(), b"bar");
130    }
131
132    #[test]
133    fn test_comm_name() {
134        assert_eq!(TaskCommand::new(b"short").comm_name(), b"short");
135        assert_eq!(TaskCommand::new(b"0123456789abcdef").comm_name(), b"0123456789abcde");
136        assert_eq!(TaskCommand::new(b"0123456789abcdefg").comm_name(), b"0123456789abcde");
137    }
138
139    #[test]
140    fn test_prctl_name() {
141        assert_eq!(TaskCommand::new(b"short").prctl_name(), *b"short\0\0\0\0\0\0\0\0\0\0\0");
142        assert_eq!(TaskCommand::new(b"0123456789abcdef").prctl_name(), *b"0123456789abcde\0");
143        assert_eq!(TaskCommand::new(b"0123456789abcdefg").prctl_name(), *b"0123456789abcde\0");
144    }
145
146    #[test]
147    fn test_prctl_name_16_bytes() {
148        let name = b"0123456789abcdef"; // 16 bytes
149        assert_eq!(TaskCommand::new(name).prctl_name(), *b"0123456789abcde\0");
150        assert_eq!(TaskCommand::new(name).comm_name(), b"0123456789abcde"); // 15 bytes
151    }
152
153    #[test]
154    fn test_debug() {
155        assert_eq!(format!("{:?}", TaskCommand::new(b"foo")), "\"foo\"");
156    }
157
158    #[test]
159    fn test_display() {
160        assert_eq!(TaskCommand::new(b"foo").to_string(), "foo");
161    }
162
163    #[test]
164    fn test_sniffing() {
165        let argv0 = TaskCommand::new(b"/path/to/binary");
166        let short = TaskCommand::new(b"binary");
167        let embedded = argv0.try_embed(&short).expect("should embed");
168        assert_eq!(embedded.as_bytes(), b"/path/to/binary");
169        assert_eq!(embedded.comm_name(), b"binary");
170
171        let other = TaskCommand::new(b"other");
172        assert!(argv0.try_embed(&other).is_none());
173    }
174
175    #[test]
176    fn test_comm_name_sniffed() {
177        let long_argv0 = TaskCommand::new(b"/path/to/short_name_with_suffix");
178        let short_name = TaskCommand::new(b"short_name");
179        let embedded = long_argv0.try_embed(&short_name).expect("should embed");
180        // comm_name should be "short_name" (len 10), not truncated version of full path
181        assert_eq!(embedded.comm_name(), b"short_name");
182    }
183}